From 5f3a1a466260a32ddcc044f611e63a779ae9e971 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:07:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor(grades,diagnostic):=20=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E6=88=90=E7=BB=A9=E5=92=8C=E5=AD=A6=E6=83=85=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=E6=A8=A1=E5=9D=97=E5=AE=A1=E8=AE=A1=20P1+P2=20?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-1: 抽取 stats-service.ts,将 8 个统计计算纯函数从 data-access 层分离 P1-5: 创建 WidgetBoundary 组件 + 补齐 teacher 路由 loading.tsx/error.tsx (14 文件) P1-6: 同步架构图文档 004/005,新增 stats-service 与 widget-boundary 节点 P2-1: 补充 a11y ARIA 属性(5 图表 role=img + aria-label,2 表格 caption,3 列表 role=list,3 按钮 aria-label) P2-3: 修复班级报告 studentId 字段语义错误(schema 改为可空 + 迁移 + 代码适配) P2-4: 修复 grade_managed scope 返回空数据(改为子查询 classes 表按 gradeId 过滤) P2-5: 新增 /parent/diagnostic/ 页面(多子女学情诊断聚合 + loading + error) P2-6: 统一 SearchParams 工具(student/grades 和 management/grade/insights 改用 @/shared/lib/search-params) --- .../004_architecture_impact_map.md | 7 +- docs/architecture/005_architecture_data.json | 196 +- .../audit/grades-diagnostic-audit-report.md | 32 +- drizzle/0003_diagnostic_student_nullable.sql | 45 + drizzle/meta/0003_snapshot.json | 7562 +++++++++++++++++ .../management/grade/insights/page.tsx | 102 +- .../(dashboard)/parent/diagnostic/error.tsx | 27 + .../(dashboard)/parent/diagnostic/loading.tsx | 32 + .../(dashboard)/parent/diagnostic/page.tsx | 70 + src/app/(dashboard)/student/grades/page.tsx | 13 +- .../diagnostic/class/[classId]/error.tsx | 27 + .../diagnostic/class/[classId]/loading.tsx | 30 + .../(dashboard)/teacher/diagnostic/error.tsx | 27 + .../teacher/diagnostic/loading.tsx | 23 + .../diagnostic/student/[studentId]/error.tsx | 27 + .../student/[studentId]/loading.tsx | 30 + .../teacher/grades/analytics/error.tsx | 27 + .../teacher/grades/analytics/loading.tsx | 31 + .../teacher/grades/entry/error.tsx | 27 + .../teacher/grades/entry/loading.tsx | 23 + src/app/(dashboard)/teacher/grades/error.tsx | 27 + .../(dashboard)/teacher/grades/loading.tsx | 28 + .../teacher/grades/stats/error.tsx | 27 + .../teacher/grades/stats/loading.tsx | 33 + .../components/mastery-radar-chart.tsx | 60 +- .../diagnostic/components/report-list.tsx | 5 +- .../components/student-diagnostic-view.tsx | 6 +- src/modules/diagnostic/data-access-reports.ts | 15 +- src/modules/diagnostic/types.ts | 4 +- .../components/class-comparison-chart.tsx | 36 +- .../components/grade-distribution-chart.tsx | 64 +- .../grades/components/grade-record-list.tsx | 16 +- .../grades/components/grade-trend-chart.tsx | 34 +- .../components/subject-comparison-chart.tsx | 46 +- .../grades/components/widget-boundary.tsx | 139 + src/modules/grades/data-access-analytics.ts | 107 +- src/modules/grades/data-access-ranking.ts | 38 +- src/modules/grades/data-access.ts | 40 +- src/modules/grades/lib/grade-utils.ts | 14 +- src/modules/grades/stats-service.ts | 305 + src/shared/db/schema.ts | 22 +- 41 files changed, 9043 insertions(+), 381 deletions(-) create mode 100644 drizzle/0003_diagnostic_student_nullable.sql create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 src/app/(dashboard)/parent/diagnostic/error.tsx create mode 100644 src/app/(dashboard)/parent/diagnostic/loading.tsx create mode 100644 src/app/(dashboard)/parent/diagnostic/page.tsx create mode 100644 src/app/(dashboard)/teacher/diagnostic/class/[classId]/error.tsx create mode 100644 src/app/(dashboard)/teacher/diagnostic/class/[classId]/loading.tsx create mode 100644 src/app/(dashboard)/teacher/diagnostic/error.tsx create mode 100644 src/app/(dashboard)/teacher/diagnostic/loading.tsx create mode 100644 src/app/(dashboard)/teacher/diagnostic/student/[studentId]/error.tsx create mode 100644 src/app/(dashboard)/teacher/diagnostic/student/[studentId]/loading.tsx create mode 100644 src/app/(dashboard)/teacher/grades/analytics/error.tsx create mode 100644 src/app/(dashboard)/teacher/grades/analytics/loading.tsx create mode 100644 src/app/(dashboard)/teacher/grades/entry/error.tsx create mode 100644 src/app/(dashboard)/teacher/grades/entry/loading.tsx create mode 100644 src/app/(dashboard)/teacher/grades/error.tsx create mode 100644 src/app/(dashboard)/teacher/grades/loading.tsx create mode 100644 src/app/(dashboard)/teacher/grades/stats/error.tsx create mode 100644 src/app/(dashboard)/teacher/grades/stats/loading.tsx create mode 100644 src/modules/grades/components/widget-boundary.tsx create mode 100644 src/modules/grades/stats-service.ts diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index 62d65da..dc176ce 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -694,7 +694,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P1-3 已修复:~~12 个查询/分析 Action 缺少 Zod 校验~~ 新增 12 个查询 schema(DeleteGradeRecordSchema/GetGradeRecordByIdSchema/GradeQuerySchema/ClassGradeStatsQuerySchema/StudentGradeSummaryQuerySchema/ClassRankingQuerySchema/ExportGradesSchema/GradeTrendQuerySchema/ClassComparisonQuerySchema/SubjectComparisonQuerySchema/GradeDistributionQuerySchema/RankingTrendQuerySchema),所有 Action 使用 safeParse 校验 - ✅ P1-4 已修复:~~`batch-grade-entry.tsx`/`grade-record-form.tsx`/`grade-distribution-chart.tsx` 中存在 `as` 断言~~ 改用类型守卫函数(isGradeType/isSemester/isDistributionTooltipPayload) - ✅ P1-5 已修复:~~teacher/grades 与 teacher/diagnostic 路由缺少 loading.tsx/error.tsx~~ 已为 7 个路由补齐 loading.tsx + error.tsx,并新增 `WidgetBoundary` 通用组件 +- ✅ P2-1 已修复:~~图表/表格/列表缺少 a11y ARIA 属性~~ 为 4 个成绩图表添加 `role="img"` + `aria-label`,2 个表格添加 ``,3 个图标按钮添加 `aria-label` - ✅ P2-2 已修复:~~diagnostic 组件中存在 Tailwind 任意值~~ 改用标准 Tailwind 类 +- ✅ P2-4 已修复:~~`buildScopeClassFilter` 中 `grade_managed` scope 返回 `sql\`1=0\``(空数据)~~ 改为子查询 `classId IN (SELECT id FROM classes WHERE grade_id IN (...))` 过滤所管年级的班级 +- ✅ P2-6 已修复:~~student/grades 和 management/grade/insights 各自重复定义 `SearchParams` 类型与 `getParam` 函数~~ 改为统一从 `@/shared/lib/search-params` 导入 - ✅ actions 层无直接 DB 访问(标杆) - ✅ data-access 按职责拆分为 3 个文件(标杆) - ✅ P2 已修复:`export.ts` 中 `scoreMap.get(r.studentId)!` 非空断言清理为安全守卫(`if (!subjMap) continue`) @@ -1312,6 +1315,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" |------|------|------| | `/parent/dashboard` | `dashboard/page.tsx` + `loading.tsx` | 家长仪表盘 | | `/parent/grades` | `grades/page.tsx` + `loading.tsx` | 多子女成绩聚合 | +| `/parent/diagnostic` | `diagnostic/page.tsx` + `loading.tsx` + `error.tsx` | P2-5 新增:多子女学情诊断聚合 | | `/parent/attendance` | `attendance/page.tsx` + `loading.tsx` | 多子女考勤聚合(v4 新增预警横幅) | | `/parent/leave` | `leave/page.tsx` + `loading.tsx` | v4 新增:请假申请(占位) | | `/parent/children/[studentId]` | `children/[studentId]/page.tsx` + `loading.tsx` | 子女详情页(v4 重构:Tab 布局 + 多子女切换) | @@ -1420,7 +1424,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P2 已修复:~~`getDiagnosticReports` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注 - ✅ P0-2 已修复:~~`data-access-reports.ts` 直查 `users` 表获取姓名~~ 改为通过 `users/data-access.getUserNamesByIds` 跨模块接口 - ✅ P2-2 已修复:~~`class-diagnostic-view.tsx`/`student-diagnostic-view.tsx`/`mastery-radar-chart.tsx` 中存在 Tailwind 任意值~~ 改用标准 Tailwind 类(w-44/max-w-32/text-xs/h-96/max-w-lg) -- ⚠️ P2:班级报告将生成者 ID 存入 `studentId` 字段(schema 设计缺陷 workaround) +- ✅ P2-1 已修复:~~图表/表格/列表缺少 a11y ARIA 属性~~ 为 5 个图表添加 `role="img"` + `aria-label`,2 个表格添加 ``,3 个列表添加 `role="list"`,3 个图标按钮添加 `aria-label` +- ✅ P2-3 已修复:~~班级报告将生成者 ID 存入 `studentId` 字段(schema 设计缺陷 workaround)~~ schema `learningDiagnosticReports.studentId` 改为可空,班级报告 `studentId` 置空,读取逻辑适配 null - ✅ 与 grades 模块无职责重叠(grades 管分数,diagnostic 管知识点掌握度) **文件清单**: diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index 1bbc14d..6f25fb4 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -6184,6 +6184,104 @@ "usedBy": [ "components/password-change-form.tsx" ] + }, + { + "name": "updateUserAvatarAction", + "file": "actions-avatar.ts", + "permission": "USER_PROFILE_UPDATE", + "signature": "(imageUrl: string) => Promise>", + "purpose": "更新用户头像 URL(P2-8 新增:文件上传通过 /api/upload 路由完成,此 action 仅更新 users.image 字段)", + "deps": [ + "requirePermission", + "users/data-access.updateUserAvatar" + ], + "usedBy": [ + "components/avatar-upload.tsx" + ] + }, + { + "name": "removeUserAvatarAction", + "file": "actions-avatar.ts", + "permission": "USER_PROFILE_UPDATE", + "signature": "() => Promise>", + "purpose": "移除用户头像(P2-8 新增)", + "deps": [ + "requirePermission", + "users/data-access.updateUserAvatar" + ], + "usedBy": [ + "components/avatar-upload.tsx" + ] + }, + { + "name": "sendTestNotificationAction", + "file": "actions-notifications.ts", + "permission": "USER_PROFILE_UPDATE", + "signature": "(input: { channel: 'push' | 'email' | 'sms' }) => Promise>", + "purpose": "发送测试通知(P2-10 新增:占位实现,待接入真实通知发送服务)", + "deps": [ + "requirePermission" + ], + "usedBy": [ + "components/notification-preferences-form.tsx" + ] + }, + { + "name": "getAdminSystemSettingsAction", + "file": "actions-system-settings.ts", + "permission": "SETTINGS_ADMIN", + "signature": "() => Promise>", + "purpose": "获取管理员系统设置(P0-3 新增:从 system_settings 表加载 4 个分类配置)", + "deps": [ + "requirePermission", + "data-access-system-settings.getAllSystemSettings" + ], + "usedBy": [ + "components/admin-settings-view.tsx" + ] + }, + { + "name": "saveAdminSystemSettingsAction", + "file": "actions-system-settings.ts", + "permission": "SETTINGS_ADMIN", + "signature": "(values: AdminSettingsFormValues) => Promise>", + "purpose": "保存管理员系统设置(P0-3 新增:4 分类 Zod 校验 + 批量 upsert)", + "deps": [ + "requirePermission", + "data-access-system-settings.upsertSystemSettings" + ], + "usedBy": [ + "components/admin-settings-view.tsx" + ] + }, + { + "name": "getSecurityCenterAction", + "file": "actions-security.ts", + "permission": "USER_PROFILE_UPDATE", + "signature": "() => Promise>", + "purpose": "获取安全中心数据(P2-9 新增:2FA 状态 + 最近 10 条登录历史)", + "deps": [ + "requirePermission", + "data-access-system-settings.getSystemSetting", + "shared.db.schema.loginLogs" + ], + "usedBy": [ + "components/security-center-card.tsx" + ] + }, + { + "name": "toggleTwoFactorAction", + "file": "actions-security.ts", + "permission": "USER_PROFILE_UPDATE", + "signature": "(enabled: boolean) => Promise>", + "purpose": "启用/禁用 2FA(P2-9 新增:占位实现,仅记录用户偏好,未接入真实 TOTP 校验)", + "deps": [ + "requirePermission", + "data-access-system-settings.upsertSystemSetting" + ], + "usedBy": [ + "components/security-center-card.tsx" + ] } ], "dataAccess": [ @@ -6276,6 +6374,56 @@ "shared.db", "shared.db.schema.passwordSecurity" ] + }, + { + "name": "getSystemSettingsByCategory", + "signature": "(category: SystemSettingCategory) => Promise", + "file": "data-access-system-settings.ts", + "purpose": "获取指定分类下所有设置项(P0-3 新增)", + "deps": [ + "shared.db", + "shared.db.schema.systemSettings" + ] + }, + { + "name": "getAllSystemSettings", + "signature": "() => Promise", + "file": "data-access-system-settings.ts", + "purpose": "获取所有系统设置项(P0-3 新增)", + "deps": [ + "shared.db", + "shared.db.schema.systemSettings" + ] + }, + { + "name": "getSystemSetting", + "signature": "(category: SystemSettingCategory, key: string) => Promise", + "file": "data-access-system-settings.ts", + "purpose": "获取单个设置项(P0-3 新增)", + "deps": [ + "shared.db", + "shared.db.schema.systemSettings" + ] + }, + { + "name": "upsertSystemSetting", + "signature": "(params: { category, key, value, valueType, updatedBy? }) => Promise", + "file": "data-access-system-settings.ts", + "purpose": "插入或更新设置项(P0-3 新增)", + "deps": [ + "shared.db", + "shared.db.schema.systemSettings" + ] + }, + { + "name": "upsertSystemSettings", + "signature": "(items: Array<{ category, key, value, valueType }>, updatedBy?: string) => Promise", + "file": "data-access-system-settings.ts", + "purpose": "批量 upsert 设置项(P0-3 新增)", + "deps": [ + "shared.db", + "shared.db.schema.systemSettings" + ] } ], "types": [ @@ -6347,11 +6495,39 @@ }, { "name": "AdminSettingsView", - "purpose": "系统设置视图(学校信息/安全策略/文件上传/通知配置 4 个 Card,i18n;消费方:/admin/settings 页面)", + "purpose": "系统设置视图(P0-3 已修复:从 mock 改为真实数据层,通过 Server Actions 加载/保存到 system_settings 表;4 个 Card:学校信息/安全策略/文件上传/通知配置;i18n:settings.admin)", + "deps": [ + "getAdminSystemSettingsAction", + "saveAdminSystemSettingsAction" + ], "usedBy": [ "app/(dashboard)/admin/settings/page.tsx" ] }, + { + "name": "AvatarUpload", + "file": "components/avatar-upload.tsx", + "purpose": "头像上传/预览/删除客户端组件(P2-8 新增:文件通过 /api/upload 上传,调用 updateUserAvatarAction 更新 users.image;验证 JPEG/PNG/WebP/GIF + 2MB 上限;i18n:settings.profile.avatar)", + "deps": [ + "updateUserAvatarAction", + "removeUserAvatarAction" + ], + "usedBy": [ + "app/(dashboard)/profile/page.tsx" + ] + }, + { + "name": "SecurityCenterCard", + "file": "components/security-center-card.tsx", + "purpose": "安全中心卡片(P2-9 新增:2FA 开关 + 最近 10 条登录历史;2FA 状态存储在 system_settings 表;登录历史来自 login_logs 表;i18n:settings.security.center)", + "deps": [ + "getSecurityCenterAction", + "toggleTwoFactorAction" + ], + "usedBy": [ + "components/settings-view.tsx" + ] + }, { "name": "ProfileSettingsForm", "purpose": "个人资料设置表单(通过 useSettingsService().profile.updateProfile 调用,i18n:settings.profile)", @@ -6363,7 +6539,10 @@ }, { "name": "ThemePreferencesCard", - "purpose": "主题偏好卡片(i18n:settings.appearance)" + "purpose": "主题偏好卡片(i18n:settings.appearance;P2-11 已增强:集成 LocaleSwitcher 语言切换到 Appearance 标签页)", + "deps": [ + "shared/components/locale-switcher" + ] }, { "name": "SettingsView", @@ -6436,7 +6615,7 @@ { "name": "NotificationPreferencesForm", "file": "components/notification-preferences-form.tsx", - "purpose": "通知偏好设置表单(Switch 切换 email/sms/push 通道 + 5 个分类开关;通过 useSettingsService().notifications.updatePreferences 调用,i18n:settings.notifications)", + "purpose": "通知偏好设置表单(Switch 切换 email/sms/push 通道 + 5 个分类开关;通过 useSettingsService().notifications.updatePreferences 调用,i18n:settings.notifications;P2-10 已增强:每个已启用渠道旁显示测试按钮,调用 sendTestNotificationAction)", "deps": [ "useSettingsService", "shared/components/ui/switch", @@ -15313,6 +15492,17 @@ "permission": "grade_record:read", "description": "家长成绩视图(按 DataScope.children 过滤;v4 新增 ParentExportButton 占位)" }, + "/parent/diagnostic": { + "component": "子女学情诊断", + "type": "server", + "module": "diagnostic", + "dataAccess": [ + "diagnostic/data-access.getStudentMasterySummary", + "diagnostic/data-access-reports.getDiagnosticReports" + ], + "permission": "diagnostic:read", + "description": "P2-5 新增:家长查看多子女学情诊断(按 DataScope.children 遍历,复用 StudentDiagnosticView 组件;含 loading.tsx + error.tsx)" + }, "/parent/attendance": { "component": "StudentAttendanceView (per child) + ParentAttendanceWarning", "type": "server", diff --git a/docs/architecture/audit/grades-diagnostic-audit-report.md b/docs/architecture/audit/grades-diagnostic-audit-report.md index 04d54dd..97dee7e 100644 --- a/docs/architecture/audit/grades-diagnostic-audit-report.md +++ b/docs/architecture/audit/grades-diagnostic-audit-report.md @@ -326,25 +326,25 @@ ### P1(较严重 — 架构与质量) -| # | 问题 | 改进方向 | -|---|------|----------| -| P1-1 | 统计业务逻辑混入 data-access | 抽取 `grades/stats-service.ts`,将 `getClassGradeStats`/`getClassComparison`/`getSubjectComparison`/`getGradeDistribution`/`getRankingTrend` 的统计计算迁移至纯函数(参考 homework/stats-service.ts 范例) | -| P1-2 | 重复工具函数 | 抽取 `grades/lib/scope-filter.ts`(`buildScopeClassFilter`)和 `grades/lib/stats-utils.ts`(`toNumber`/`normalize`) | -| P1-3 | 12 个 Action 缺失 Zod 校验 | 为 `deleteGradeRecordAction`/`getGradeRecordsAction`/`getClassGradeStatsAction`/`getStudentGradeSummaryAction`/`getClassRankingAction`/`getGradeRecordByIdAction`/`exportGradesAction` + 5 个 analytics Action 创建对应 Zod schema | -| P1-4 | `as` 断言违规(4 处) | 使用类型守卫或 Zod 运行时校验替代 | -| P1-5 | Error Boundary 和 Suspense 缺失 | 创建 `grades/components/widget-boundary.tsx`(Error Boundary + Suspense + Skeleton 组合);每个数据区块独立包裹;teacher/grades 和 teacher/diagnostic 路由补齐 loading.tsx/error.tsx | -| P1-6 | 架构图同步 | 更新 `004` 和 `005` 文档:grades 行数、diagnostic deps、新增 stats-service.ts、新增 lib/、补齐 actions-analytics exports | +| # | 问题 | 改进方向 | 状态 | +|---|------|----------|------| +| P1-1 | 统计业务逻辑混入 data-access | 抽取 `grades/stats-service.ts`,将 `getClassGradeStats`/`getClassComparison`/`getSubjectComparison`/`getGradeDistribution`/`getRankingTrend` 的统计计算迁移至纯函数(参考 homework/stats-service.ts 范例) | ✅ 已完成 | +| P1-2 | 重复工具函数 | 抽取 `grades/lib/scope-filter.ts`(`buildScopeClassFilter`)和 `grades/lib/stats-utils.ts`(`toNumber`/`normalize`) | ✅ 已完成 | +| P1-3 | 12 个 Action 缺失 Zod 校验 | 为 `deleteGradeRecordAction`/`getGradeRecordsAction`/`getClassGradeStatsAction`/`getStudentGradeSummaryAction`/`getClassRankingAction`/`getGradeRecordByIdAction`/`exportGradesAction` + 5 个 analytics Action 创建对应 Zod schema | ✅ 已完成 | +| P1-4 | `as` 断言违规(4 处) | 使用类型守卫或 Zod 运行时校验替代 | ✅ 已完成 | +| P1-5 | Error Boundary 和 Suspense 缺失 | 创建 `grades/components/widget-boundary.tsx`(Error Boundary + Suspense + Skeleton 组合);每个数据区块独立包裹;teacher/grades 和 teacher/diagnostic 路由补齐 loading.tsx/error.tsx | ✅ 已完成 | +| P1-6 | 架构图同步 | 更新 `004` 和 `005` 文档:grades 行数、diagnostic deps、新增 stats-service.ts、新增 lib/、补齐 actions-analytics exports | ✅ 已完成 | ### P2(优化 — 体验与扩展) -| # | 问题 | 改进方向 | -|---|------|----------| -| P2-1 | a11y 无障碍缺失 | 补充 ARIA 属性:图表 `role="img"` + `aria-label`;按钮 `aria-label`;表格 `caption`;列表 `role="list"` | -| P2-2 | Tailwind 任意值 | 移除 `w-[180px]`/`h-[360px]`/`max-w-[520px]`,改用设计令牌或注释说明 | -| P2-3 | 班级报告 studentId 字段语义错误 | 修改 `learningDiagnosticReports` schema,将 `studentId` 改为可空,或增加 `classId`/`generatedBy` 字段 | -| P2-4 | grade_managed scope 返回空数据 | 修复 `buildScopeClassFilter`,grade_managed scope 应返回所管年级的班级过滤条件 | -| P2-5 | admin/parent 无 diagnostic UI | 新增 `/parent/diagnostic/` 页面(家长查看子女诊断报告);admin 可复用 teacher 视图 | -| P2-6 | SearchParams 工具未统一 | student/grades 和 management/grade/insights 改用 `@/shared/lib/search-params` | +| # | 问题 | 改进方向 | 状态 | +|---|------|----------|------| +| P2-1 | a11y 无障碍缺失 | 补充 ARIA 属性:图表 `role="img"` + `aria-label`;按钮 `aria-label`;表格 `caption`;列表 `role="list"` | ✅ 已完成 | +| P2-2 | Tailwind 任意值 | 移除 `w-[180px]`/`h-[360px]`/`max-w-[520px]`,改用设计令牌或注释说明 | ✅ 已完成 | +| P2-3 | 班级报告 studentId 字段语义错误 | 修改 `learningDiagnosticReports` schema,将 `studentId` 改为可空,或增加 `classId`/`generatedBy` 字段 | ✅ 已完成 | +| P2-4 | grade_managed scope 返回空数据 | 修复 `buildScopeClassFilter`,grade_managed scope 应返回所管年级的班级过滤条件 | ✅ 已完成 | +| P2-5 | admin/parent 无 diagnostic UI | 新增 `/parent/diagnostic/` 页面(家长查看子女诊断报告);admin 可复用 teacher 视图 | ✅ 已完成 | +| P2-6 | SearchParams 工具未统一 | student/grades 和 management/grade/insights 改用 `@/shared/lib/search-params` | ✅ 已完成 | ### P3(长期 — 行业对标) diff --git a/drizzle/0003_diagnostic_student_nullable.sql b/drizzle/0003_diagnostic_student_nullable.sql new file mode 100644 index 0000000..47c8241 --- /dev/null +++ b/drizzle/0003_diagnostic_student_nullable.sql @@ -0,0 +1,45 @@ +CREATE TABLE `class_invitation_codes` ( + `id` varchar(128) NOT NULL, + `class_id` varchar(128) NOT NULL, + `code` varchar(8) NOT NULL, + `class_invitation_code_status` enum('active','disabled','expired','exhausted') NOT NULL DEFAULT 'active', + `max_uses` int, + `used_count` int NOT NULL DEFAULT 0, + `expires_at` timestamp, + `created_by` varchar(128) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT (now()), + `updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + `revoked_at` timestamp, + `revoked_by` varchar(128), + `note` varchar(255), + CONSTRAINT `class_invitation_codes_id` PRIMARY KEY(`id`), + CONSTRAINT `class_invitation_codes_code_unique` UNIQUE(`code`), + CONSTRAINT `class_invitation_codes_code_idx` UNIQUE(`code`) +); +--> statement-breakpoint +CREATE TABLE `system_settings` ( + `id` varchar(128) NOT NULL, + `category` varchar(50) NOT NULL, + `key` varchar(100) NOT NULL, + `value` text NOT NULL, + `value_type` varchar(20) NOT NULL DEFAULT 'string', + `updated_by` varchar(128), + `created_at` timestamp NOT NULL DEFAULT (now()), + `updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `system_settings_id` PRIMARY KEY(`id`), + CONSTRAINT `ss_category_key_idx` UNIQUE(`category`,`key`) +); +--> statement-breakpoint +ALTER TABLE `homework_assignments` MODIFY COLUMN `source_exam_id` varchar(128);--> statement-breakpoint +ALTER TABLE `learning_diagnostic_reports` MODIFY COLUMN `student_id` varchar(128);--> statement-breakpoint +ALTER TABLE `messages` ADD `sender_deleted_at` timestamp;--> statement-breakpoint +ALTER TABLE `messages` ADD `receiver_deleted_at` timestamp;--> statement-breakpoint +ALTER TABLE `notification_preferences` ADD `quiet_hours_enabled` boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `notification_preferences` ADD `quiet_hours_start` varchar(5);--> statement-breakpoint +ALTER TABLE `notification_preferences` ADD `quiet_hours_end` varchar(5);--> statement-breakpoint +ALTER TABLE `class_invitation_codes` ADD CONSTRAINT `class_invitation_codes_class_id_classes_id_fk` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `class_invitation_codes` ADD CONSTRAINT `class_invitation_codes_created_by_users_id_fk` FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `class_invitation_codes` ADD CONSTRAINT `cic_c_fk` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `class_invitation_codes_class_idx` ON `class_invitation_codes` (`class_id`);--> statement-breakpoint +CREATE INDEX `class_invitation_codes_status_expires_idx` ON `class_invitation_codes` (`class_invitation_code_status`,`expires_at`);--> statement-breakpoint +CREATE INDEX `ss_category_idx` ON `system_settings` (`category`); \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..675074f --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,7562 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "6fe72813-5ec3-4b06-a7d1-a475e3a06ddf", + "prevId": "f2b1af65-541e-41c5-9393-a5087fa3ca5f", + "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": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "enum('zhipu','openai','gemini','custom')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key_encrypted": { + "name": "api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key_last4": { + "name": "api_key_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "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": { + "ai_provider_idx": { + "name": "ai_provider_idx", + "columns": [ + "provider" + ], + "isUnique": false + }, + "ai_provider_default_idx": { + "name": "ai_provider_default_idx", + "columns": [ + "is_default" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ai_providers_id": { + "name": "ai_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "announcements": { + "name": "announcements", + "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 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('school','grade','class')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'school'" + }, + "status": { + "name": "status", + "type": "enum('draft','published','archived')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "target_grade_id": { + "name": "target_grade_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_class_id": { + "name": "target_class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "datetime", + "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": { + "announcements_author_idx": { + "name": "announcements_author_idx", + "columns": [ + "author_id" + ], + "isUnique": false + }, + "announcements_status_idx": { + "name": "announcements_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "announcements_type_idx": { + "name": "announcements_type_idx", + "columns": [ + "type" + ], + "isUnique": false + }, + "announcements_target_grade_idx": { + "name": "announcements_target_grade_idx", + "columns": [ + "target_grade_id" + ], + "isUnique": false + }, + "announcements_target_class_idx": { + "name": "announcements_target_class_idx", + "columns": [ + "target_class_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "announcements_author_id_users_id_fk": { + "name": "announcements_author_id_users_id_fk", + "tableFrom": "announcements", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "announcements_id": { + "name": "announcements_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "attendance_records": { + "name": "attendance_records", + "columns": { + "id": { + "name": "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_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('present','absent','late','early_leave','excused')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recorded_by": { + "name": "recorded_by", + "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": { + "attendance_records_student_idx": { + "name": "attendance_records_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + }, + "attendance_records_class_idx": { + "name": "attendance_records_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "attendance_records_date_idx": { + "name": "attendance_records_date_idx", + "columns": [ + "date" + ], + "isUnique": false + }, + "attendance_records_class_date_idx": { + "name": "attendance_records_class_date_idx", + "columns": [ + "class_id", + "date" + ], + "isUnique": false + }, + "attendance_records_student_date_idx": { + "name": "attendance_records_student_date_idx", + "columns": [ + "student_id", + "date" + ], + "isUnique": false + }, + "attendance_records_schedule_idx": { + "name": "attendance_records_schedule_idx", + "columns": [ + "schedule_id" + ], + "isUnique": false + }, + "attendance_records_recorded_by_idx": { + "name": "attendance_records_recorded_by_idx", + "columns": [ + "recorded_by" + ], + "isUnique": false + } + }, + "foreignKeys": { + "attendance_records_student_id_users_id_fk": { + "name": "attendance_records_student_id_users_id_fk", + "tableFrom": "attendance_records", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "attendance_records_class_id_classes_id_fk": { + "name": "attendance_records_class_id_classes_id_fk", + "tableFrom": "attendance_records", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "attendance_records_recorded_by_users_id_fk": { + "name": "attendance_records_recorded_by_users_id_fk", + "tableFrom": "attendance_records", + "tableTo": "users", + "columnsFrom": [ + "recorded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ar_c_fk": { + "name": "ar_c_fk", + "tableFrom": "attendance_records", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ar_s_fk": { + "name": "ar_s_fk", + "tableFrom": "attendance_records", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ar_rb_fk": { + "name": "ar_rb_fk", + "tableFrom": "attendance_records", + "tableTo": "users", + "columnsFrom": [ + "recorded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "attendance_records_id": { + "name": "attendance_records_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "attendance_rules": { + "name": "attendance_rules", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "late_threshold_minutes": { + "name": "late_threshold_minutes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 15 + }, + "early_leave_threshold_minutes": { + "name": "early_leave_threshold_minutes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 15 + }, + "enable_auto_mark": { + "name": "enable_auto_mark", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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": { + "attendance_rules_class_idx": { + "name": "attendance_rules_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "attendance_rules_class_id_classes_id_fk": { + "name": "attendance_rules_class_id_classes_id_fk", + "tableFrom": "attendance_rules", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "atr_c_fk": { + "name": "atr_c_fk", + "tableFrom": "attendance_rules", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "attendance_rules_id": { + "name": "attendance_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "module": { + "name": "module", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('success','failure')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'success'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_module_idx": { + "name": "audit_logs_module_idx", + "columns": [ + "module" + ], + "isUnique": false + }, + "audit_logs_action_idx": { + "name": "audit_logs_action_idx", + "columns": [ + "action" + ], + "isUnique": false + }, + "audit_logs_status_idx": { + "name": "audit_logs_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "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_invitation_codes": { + "name": "class_invitation_codes", + "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 + }, + "code": { + "name": "code", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_invitation_code_status": { + "name": "class_invitation_code_status", + "type": "enum('active','disabled','expired','exhausted')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "max_uses": { + "name": "max_uses", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_count": { + "name": "used_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "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())" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "class_invitation_codes_code_idx": { + "name": "class_invitation_codes_code_idx", + "columns": [ + "code" + ], + "isUnique": true + }, + "class_invitation_codes_class_idx": { + "name": "class_invitation_codes_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "class_invitation_codes_status_expires_idx": { + "name": "class_invitation_codes_status_expires_idx", + "columns": [ + "class_invitation_code_status", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "class_invitation_codes_class_id_classes_id_fk": { + "name": "class_invitation_codes_class_id_classes_id_fk", + "tableFrom": "class_invitation_codes", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "class_invitation_codes_created_by_users_id_fk": { + "name": "class_invitation_codes_created_by_users_id_fk", + "tableFrom": "class_invitation_codes", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cic_c_fk": { + "name": "cic_c_fk", + "tableFrom": "class_invitation_codes", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "class_invitation_codes_id": { + "name": "class_invitation_codes_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "class_invitation_codes_code_unique": { + "name": "class_invitation_codes_code_unique", + "columns": [ + "code" + ] + } + }, + "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_id": { + "name": "subject_id", + "type": "varchar(128)", + "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 + }, + "class_subject_teachers_subject_id_idx": { + "name": "class_subject_teachers_subject_id_idx", + "columns": [ + "subject_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" + }, + "cst_s_fk": { + "name": "cst_s_fk", + "tableFrom": "class_subject_teachers", + "tableTo": "subjects", + "columnsFrom": [ + "subject_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "class_subject_teachers_class_id_subject_id_pk": { + "name": "class_subject_teachers_class_id_subject_id_pk", + "columns": [ + "class_id", + "subject_id" + ] + } + }, + "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": {} + }, + "course_plan_items": { + "name": "course_plan_items", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_id": { + "name": "plan_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "week": { + "name": "week", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hours": { + "name": "hours", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "textbook_chapter": { + "name": "textbook_chapter", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_completed": { + "name": "is_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "completed_at": { + "name": "completed_at", + "type": "date", + "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": { + "course_plan_items_plan_idx": { + "name": "course_plan_items_plan_idx", + "columns": [ + "plan_id" + ], + "isUnique": false + }, + "course_plan_items_plan_week_idx": { + "name": "course_plan_items_plan_week_idx", + "columns": [ + "plan_id", + "week" + ], + "isUnique": false + } + }, + "foreignKeys": { + "course_plan_items_plan_id_course_plans_id_fk": { + "name": "course_plan_items_plan_id_course_plans_id_fk", + "tableFrom": "course_plan_items", + "tableTo": "course_plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cpi_p_fk": { + "name": "cpi_p_fk", + "tableFrom": "course_plan_items", + "tableTo": "course_plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "course_plan_items_id": { + "name": "course_plan_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "course_plans": { + "name": "course_plans", + "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 + }, + "subject_id": { + "name": "subject_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "teacher_id": { + "name": "teacher_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "academic_year_id": { + "name": "academic_year_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "semester": { + "name": "semester", + "type": "enum('1','2')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1'" + }, + "total_hours": { + "name": "total_hours", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "completed_hours": { + "name": "completed_hours", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "weekly_hours": { + "name": "weekly_hours", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "syllabus": { + "name": "syllabus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "objectives": { + "name": "objectives", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('planning','active','completed','paused')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'planning'" + }, + "created_by": { + "name": "created_by", + "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": { + "course_plans_class_idx": { + "name": "course_plans_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "course_plans_teacher_idx": { + "name": "course_plans_teacher_idx", + "columns": [ + "teacher_id" + ], + "isUnique": false + }, + "course_plans_subject_idx": { + "name": "course_plans_subject_idx", + "columns": [ + "subject_id" + ], + "isUnique": false + }, + "course_plans_status_idx": { + "name": "course_plans_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "course_plans_class_subject_idx": { + "name": "course_plans_class_subject_idx", + "columns": [ + "class_id", + "subject_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "course_plans_class_id_classes_id_fk": { + "name": "course_plans_class_id_classes_id_fk", + "tableFrom": "course_plans", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "course_plans_subject_id_subjects_id_fk": { + "name": "course_plans_subject_id_subjects_id_fk", + "tableFrom": "course_plans", + "tableTo": "subjects", + "columnsFrom": [ + "subject_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "course_plans_teacher_id_users_id_fk": { + "name": "course_plans_teacher_id_users_id_fk", + "tableFrom": "course_plans", + "tableTo": "users", + "columnsFrom": [ + "teacher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "course_plans_created_by_users_id_fk": { + "name": "course_plans_created_by_users_id_fk", + "tableFrom": "course_plans", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cp_c_fk": { + "name": "cp_c_fk", + "tableFrom": "course_plans", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cp_s_fk": { + "name": "cp_s_fk", + "tableFrom": "course_plans", + "tableTo": "subjects", + "columnsFrom": [ + "subject_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cp_t_fk": { + "name": "cp_t_fk", + "tableFrom": "course_plans", + "tableTo": "users", + "columnsFrom": [ + "teacher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cp_cb_fk": { + "name": "cp_cb_fk", + "tableFrom": "course_plans", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "course_plans_id": { + "name": "course_plans_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "course_selections": { + "name": "course_selections", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "course_id": { + "name": "course_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "selection_status": { + "name": "selection_status", + "type": "enum('selected','enrolled','waitlist','dropped','rejected')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'selected'" + }, + "priority": { + "name": "priority", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1 + }, + "selected_at": { + "name": "selected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lottery_rank": { + "name": "lottery_rank", + "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": { + "course_selections_course_idx": { + "name": "course_selections_course_idx", + "columns": [ + "course_id" + ], + "isUnique": false + }, + "course_selections_student_idx": { + "name": "course_selections_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + }, + "course_selections_status_idx": { + "name": "course_selections_status_idx", + "columns": [ + "selection_status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "course_selections_course_id_elective_courses_id_fk": { + "name": "course_selections_course_id_elective_courses_id_fk", + "tableFrom": "course_selections", + "tableTo": "elective_courses", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "course_selections_student_id_users_id_fk": { + "name": "course_selections_student_id_users_id_fk", + "tableFrom": "course_selections", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "course_selections_id": { + "name": "course_selections_id", + "columns": [ + "id" + ] + }, + "course_selections_course_id_student_id_pk": { + "name": "course_selections_course_id_student_id_pk", + "columns": [ + "course_id", + "student_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "data_change_logs": { + "name": "data_change_logs", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_name": { + "name": "table_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_id": { + "name": "record_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "enum('create','update','delete')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "old_value": { + "name": "old_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_value": { + "name": "new_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "changed_by": { + "name": "changed_by", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "changed_by_name": { + "name": "changed_by_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "data_change_logs_table_name_idx": { + "name": "data_change_logs_table_name_idx", + "columns": [ + "table_name" + ], + "isUnique": false + }, + "data_change_logs_record_id_idx": { + "name": "data_change_logs_record_id_idx", + "columns": [ + "record_id" + ], + "isUnique": false + }, + "data_change_logs_action_idx": { + "name": "data_change_logs_action_idx", + "columns": [ + "action" + ], + "isUnique": false + }, + "data_change_logs_changed_by_idx": { + "name": "data_change_logs_changed_by_idx", + "columns": [ + "changed_by" + ], + "isUnique": false + }, + "data_change_logs_created_at_idx": { + "name": "data_change_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "data_change_logs_id": { + "name": "data_change_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "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": {} + }, + "elective_courses": { + "name": "elective_courses", + "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 + }, + "subject_id": { + "name": "subject_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teacher_id": { + "name": "teacher_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grade_id": { + "name": "grade_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "capacity": { + "name": "capacity", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "enrolled_count": { + "name": "enrolled_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "classroom": { + "name": "classroom", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "schedule": { + "name": "schedule", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selection_start_at": { + "name": "selection_start_at", + "type": "datetime", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selection_end_at": { + "name": "selection_end_at", + "type": "datetime", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('draft','open','closed','cancelled')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "selection_mode": { + "name": "selection_mode", + "type": "enum('fcfs','lottery')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fcfs'" + }, + "credit": { + "name": "credit", + "type": "decimal(3,1)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'1.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": { + "elective_courses_teacher_idx": { + "name": "elective_courses_teacher_idx", + "columns": [ + "teacher_id" + ], + "isUnique": false + }, + "elective_courses_subject_idx": { + "name": "elective_courses_subject_idx", + "columns": [ + "subject_id" + ], + "isUnique": false + }, + "elective_courses_grade_idx": { + "name": "elective_courses_grade_idx", + "columns": [ + "grade_id" + ], + "isUnique": false + }, + "elective_courses_status_idx": { + "name": "elective_courses_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "elective_courses_subject_id_subjects_id_fk": { + "name": "elective_courses_subject_id_subjects_id_fk", + "tableFrom": "elective_courses", + "tableTo": "subjects", + "columnsFrom": [ + "subject_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "elective_courses_teacher_id_users_id_fk": { + "name": "elective_courses_teacher_id_users_id_fk", + "tableFrom": "elective_courses", + "tableTo": "users", + "columnsFrom": [ + "teacher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "elective_courses_grade_id_grades_id_fk": { + "name": "elective_courses_grade_id_grades_id_fk", + "tableFrom": "elective_courses", + "tableTo": "grades", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "elective_courses_id": { + "name": "elective_courses_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "exam_proctoring_events": { + "name": "exam_proctoring_events", + "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 + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exam_id": { + "name": "exam_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "enum('tab_switch','window_blur','copy_attempt','paste_attempt','right_click','devtools_open','fullscreen_exit','idle_timeout')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_detail": { + "name": "event_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "proctoring_submission_idx": { + "name": "proctoring_submission_idx", + "columns": [ + "submission_id" + ], + "isUnique": false + }, + "proctoring_student_idx": { + "name": "proctoring_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + }, + "proctoring_exam_idx": { + "name": "proctoring_exam_idx", + "columns": [ + "exam_id" + ], + "isUnique": false + }, + "proctoring_event_type_idx": { + "name": "proctoring_event_type_idx", + "columns": [ + "event_type" + ], + "isUnique": false + } + }, + "foreignKeys": { + "exam_proctoring_events_submission_id_exam_submissions_id_fk": { + "name": "exam_proctoring_events_submission_id_exam_submissions_id_fk", + "tableFrom": "exam_proctoring_events", + "tableTo": "exam_submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "exam_proctoring_events_student_id_users_id_fk": { + "name": "exam_proctoring_events_student_id_users_id_fk", + "tableFrom": "exam_proctoring_events", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "exam_proctoring_events_exam_id_exams_id_fk": { + "name": "exam_proctoring_events_exam_id_exams_id_fk", + "tableFrom": "exam_proctoring_events", + "tableTo": "exams", + "columnsFrom": [ + "exam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "exam_proctoring_events_id": { + "name": "exam_proctoring_events_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "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 + }, + "exam_mode": { + "name": "exam_mode", + "type": "enum('homework','timed','proctored')", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'homework'" + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shuffle_questions": { + "name": "shuffle_questions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "allow_late_start": { + "name": "allow_late_start", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "late_start_grace_minutes": { + "name": "late_start_grace_minutes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "anti_cheat_enabled": { + "name": "anti_cheat_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 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": {} + }, + "file_attachments": { + "name": "file_attachments", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_name": { + "name": "original_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_path": { + "name": "storage_path", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uploader_id": { + "name": "uploader_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_id": { + "name": "target_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())" + } + }, + "indexes": { + "file_attachments_uploader_idx": { + "name": "file_attachments_uploader_idx", + "columns": [ + "uploader_id" + ], + "isUnique": false + }, + "file_attachments_target_idx": { + "name": "file_attachments_target_idx", + "columns": [ + "target_type", + "target_id" + ], + "isUnique": false + }, + "file_attachments_created_at_idx": { + "name": "file_attachments_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "file_attachments_uploader_id_users_id_fk": { + "name": "file_attachments_uploader_id_users_id_fk", + "tableFrom": "file_attachments", + "tableTo": "users", + "columnsFrom": [ + "uploader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_attachments_id": { + "name": "file_attachments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "grade_records": { + "name": "grade_records", + "columns": { + "id": { + "name": "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_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exam_id": { + "name": "exam_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "academic_year_id": { + "name": "academic_year_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "decimal(6,2)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "full_score": { + "name": "full_score", + "type": "decimal(6,2)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'100'" + }, + "type": { + "name": "type", + "type": "enum('exam','quiz','homework','other')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'exam'" + }, + "semester": { + "name": "semester", + "type": "enum('1','2')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1'" + }, + "recorded_by": { + "name": "recorded_by", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remark": { + "name": "remark", + "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": { + "grade_records_student_idx": { + "name": "grade_records_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + }, + "grade_records_class_idx": { + "name": "grade_records_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "grade_records_subject_idx": { + "name": "grade_records_subject_idx", + "columns": [ + "subject_id" + ], + "isUnique": false + }, + "grade_records_exam_idx": { + "name": "grade_records_exam_idx", + "columns": [ + "exam_id" + ], + "isUnique": false + }, + "grade_records_class_subject_idx": { + "name": "grade_records_class_subject_idx", + "columns": [ + "class_id", + "subject_id" + ], + "isUnique": false + }, + "grade_records_recorded_by_idx": { + "name": "grade_records_recorded_by_idx", + "columns": [ + "recorded_by" + ], + "isUnique": false + } + }, + "foreignKeys": { + "grade_records_student_id_users_id_fk": { + "name": "grade_records_student_id_users_id_fk", + "tableFrom": "grade_records", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "grade_records_class_id_classes_id_fk": { + "name": "grade_records_class_id_classes_id_fk", + "tableFrom": "grade_records", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "grade_records_subject_id_subjects_id_fk": { + "name": "grade_records_subject_id_subjects_id_fk", + "tableFrom": "grade_records", + "tableTo": "subjects", + "columnsFrom": [ + "subject_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "grade_records_recorded_by_users_id_fk": { + "name": "grade_records_recorded_by_users_id_fk", + "tableFrom": "grade_records", + "tableTo": "users", + "columnsFrom": [ + "recorded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gr_c_fk": { + "name": "gr_c_fk", + "tableFrom": "grade_records", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gr_s_fk": { + "name": "gr_s_fk", + "tableFrom": "grade_records", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gr_sub_fk": { + "name": "gr_sub_fk", + "tableFrom": "grade_records", + "tableTo": "subjects", + "columnsFrom": [ + "subject_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gr_rb_fk": { + "name": "gr_rb_fk", + "tableFrom": "grade_records", + "tableTo": "users", + "columnsFrom": [ + "recorded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "grade_records_id": { + "name": "grade_records_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": false, + "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_point_mastery": { + "name": "knowledge_point_mastery", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_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 + }, + "mastery_level": { + "name": "mastery_level", + "type": "decimal(5,2)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'0'" + }, + "total_questions": { + "name": "total_questions", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "correct_questions": { + "name": "correct_questions", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_assessed_at": { + "name": "last_assessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "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": { + "mastery_student_idx": { + "name": "mastery_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + }, + "mastery_kp_idx": { + "name": "mastery_kp_idx", + "columns": [ + "knowledge_point_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "knowledge_point_mastery_student_id_users_id_fk": { + "name": "knowledge_point_mastery_student_id_users_id_fk", + "tableFrom": "knowledge_point_mastery", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_point_mastery_knowledge_point_id_knowledge_points_id_fk": { + "name": "knowledge_point_mastery_knowledge_point_id_knowledge_points_id_fk", + "tableFrom": "knowledge_point_mastery", + "tableTo": "knowledge_points", + "columnsFrom": [ + "knowledge_point_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_point_mastery_id": { + "name": "knowledge_point_mastery_id", + "columns": [ + "id" + ] + }, + "knowledge_point_mastery_student_id_knowledge_point_id_pk": { + "name": "knowledge_point_mastery_student_id_knowledge_point_id_pk", + "columns": [ + "student_id", + "knowledge_point_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 + }, + "anchor_text": { + "name": "anchor_text", + "type": "varchar(255)", + "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": {} + }, + "learning_diagnostic_reports": { + "name": "learning_diagnostic_reports", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "generated_by": { + "name": "generated_by", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_type": { + "name": "report_type", + "type": "enum('individual','class','grade')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'individual'" + }, + "period": { + "name": "period", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "strengths": { + "name": "strengths", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weaknesses": { + "name": "weaknesses", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recommendations": { + "name": "recommendations", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overall_score": { + "name": "overall_score", + "type": "decimal(5,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_status": { + "name": "report_status", + "type": "enum('draft','published','archived')", + "primaryKey": false, + "notNull": true, + "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": { + "diagnostic_student_idx": { + "name": "diagnostic_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + }, + "diagnostic_generated_by_idx": { + "name": "diagnostic_generated_by_idx", + "columns": [ + "generated_by" + ], + "isUnique": false + }, + "diagnostic_status_idx": { + "name": "diagnostic_status_idx", + "columns": [ + "report_status" + ], + "isUnique": false + }, + "diagnostic_report_type_idx": { + "name": "diagnostic_report_type_idx", + "columns": [ + "report_type" + ], + "isUnique": false + } + }, + "foreignKeys": { + "learning_diagnostic_reports_student_id_users_id_fk": { + "name": "learning_diagnostic_reports_student_id_users_id_fk", + "tableFrom": "learning_diagnostic_reports", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "learning_diagnostic_reports_generated_by_users_id_fk": { + "name": "learning_diagnostic_reports_generated_by_users_id_fk", + "tableFrom": "learning_diagnostic_reports", + "tableTo": "users", + "columnsFrom": [ + "generated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "learning_diagnostic_reports_id": { + "name": "learning_diagnostic_reports_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "lesson_plan_templates": { + "name": "lesson_plan_templates", + "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 + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blocks": { + "name": "blocks", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_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": { + "lpt_type_creator_idx": { + "name": "lpt_type_creator_idx", + "columns": [ + "type", + "creator_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "lesson_plan_templates_creator_id_users_id_fk": { + "name": "lesson_plan_templates_creator_id_users_id_fk", + "tableFrom": "lesson_plan_templates", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "lesson_plan_templates_id": { + "name": "lesson_plan_templates_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "lesson_plan_versions": { + "name": "lesson_plan_versions", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_id": { + "name": "plan_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version_no": { + "name": "version_no", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_auto": { + "name": "is_auto", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_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": { + "lpv_plan_version_idx": { + "name": "lpv_plan_version_idx", + "columns": [ + "plan_id", + "version_no" + ], + "isUnique": false + }, + "lpv_plan_created_idx": { + "name": "lpv_plan_created_idx", + "columns": [ + "plan_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "lesson_plan_versions_plan_id_lesson_plans_id_fk": { + "name": "lesson_plan_versions_plan_id_lesson_plans_id_fk", + "tableFrom": "lesson_plan_versions", + "tableTo": "lesson_plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lesson_plan_versions_creator_id_users_id_fk": { + "name": "lesson_plan_versions_creator_id_users_id_fk", + "tableFrom": "lesson_plan_versions", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "lesson_plan_versions_id": { + "name": "lesson_plan_versions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "lesson_plans": { + "name": "lesson_plans", + "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 + }, + "textbook_id": { + "name": "textbook_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chapter_id": { + "name": "chapter_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "course_plan_item_id": { + "name": "course_plan_item_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "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 + }, + "template_id": { + "name": "template_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "template_name": { + "name": "template_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_saved_at": { + "name": "last_saved_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": { + "lp_creator_idx": { + "name": "lp_creator_idx", + "columns": [ + "creator_id" + ], + "isUnique": false + }, + "lp_status_idx": { + "name": "lp_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "lp_textbook_chapter_idx": { + "name": "lp_textbook_chapter_idx", + "columns": [ + "textbook_id", + "chapter_id" + ], + "isUnique": false + }, + "lp_subject_grade_idx": { + "name": "lp_subject_grade_idx", + "columns": [ + "subject_id", + "grade_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "lesson_plans_textbook_id_textbooks_id_fk": { + "name": "lesson_plans_textbook_id_textbooks_id_fk", + "tableFrom": "lesson_plans", + "tableTo": "textbooks", + "columnsFrom": [ + "textbook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "lesson_plans_chapter_id_chapters_id_fk": { + "name": "lesson_plans_chapter_id_chapters_id_fk", + "tableFrom": "lesson_plans", + "tableTo": "chapters", + "columnsFrom": [ + "chapter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "lesson_plans_subject_id_subjects_id_fk": { + "name": "lesson_plans_subject_id_subjects_id_fk", + "tableFrom": "lesson_plans", + "tableTo": "subjects", + "columnsFrom": [ + "subject_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "lesson_plans_grade_id_grades_id_fk": { + "name": "lesson_plans_grade_id_grades_id_fk", + "tableFrom": "lesson_plans", + "tableTo": "grades", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "lesson_plans_creator_id_users_id_fk": { + "name": "lesson_plans_creator_id_users_id_fk", + "tableFrom": "lesson_plans", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "lesson_plans_id": { + "name": "lesson_plans_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "login_logs": { + "name": "login_logs", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_email": { + "name": "user_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "enum('signin','signout','signup')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('success','failure')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'success'" + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "login_logs_user_id_idx": { + "name": "login_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "login_logs_user_email_idx": { + "name": "login_logs_user_email_idx", + "columns": [ + "user_email" + ], + "isUnique": false + }, + "login_logs_action_idx": { + "name": "login_logs_action_idx", + "columns": [ + "action" + ], + "isUnique": false + }, + "login_logs_status_idx": { + "name": "login_logs_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "login_logs_created_at_idx": { + "name": "login_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "login_logs_id": { + "name": "login_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "message_notifications": { + "name": "message_notifications", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link": { + "name": "link", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_read": { + "name": "is_read", + "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())" + } + }, + "indexes": { + "message_notifications_user_idx": { + "name": "message_notifications_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "message_notifications_is_read_idx": { + "name": "message_notifications_is_read_idx", + "columns": [ + "is_read" + ], + "isUnique": false + }, + "message_notifications_user_read_idx": { + "name": "message_notifications_user_read_idx", + "columns": [ + "user_id", + "is_read" + ], + "isUnique": false + }, + "message_notifications_created_at_idx": { + "name": "message_notifications_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "message_notifications_user_id_users_id_fk": { + "name": "message_notifications_user_id_users_id_fk", + "tableFrom": "message_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_notifications_id": { + "name": "message_notifications_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender_id": { + "name": "sender_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "receiver_id": { + "name": "receiver_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sender_deleted_at": { + "name": "sender_deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "receiver_deleted_at": { + "name": "receiver_deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "messages_sender_idx": { + "name": "messages_sender_idx", + "columns": [ + "sender_id" + ], + "isUnique": false + }, + "messages_receiver_idx": { + "name": "messages_receiver_idx", + "columns": [ + "receiver_id" + ], + "isUnique": false + }, + "messages_is_read_idx": { + "name": "messages_is_read_idx", + "columns": [ + "is_read" + ], + "isUnique": false + }, + "messages_parent_idx": { + "name": "messages_parent_idx", + "columns": [ + "parent_message_id" + ], + "isUnique": false + }, + "messages_receiver_read_idx": { + "name": "messages_receiver_read_idx", + "columns": [ + "receiver_id", + "is_read" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_sender_id_users_id_fk": { + "name": "messages_sender_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "sender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_receiver_id_users_id_fk": { + "name": "messages_receiver_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "receiver_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "messages_id": { + "name": "messages_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "notification_preferences": { + "name": "notification_preferences", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_enabled": { + "name": "email_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sms_enabled": { + "name": "sms_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "push_enabled": { + "name": "push_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "homework_notifications": { + "name": "homework_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "grade_notifications": { + "name": "grade_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "announcement_notifications": { + "name": "announcement_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "message_notifications": { + "name": "message_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "attendance_notifications": { + "name": "attendance_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "quiet_hours_enabled": { + "name": "quiet_hours_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "quiet_hours_start": { + "name": "quiet_hours_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quiet_hours_end": { + "name": "quiet_hours_end", + "type": "varchar(5)", + "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": { + "notification_preferences_user_idx": { + "name": "notification_preferences_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notification_preferences_user_id_users_id_fk": { + "name": "notification_preferences_user_id_users_id_fk", + "tableFrom": "notification_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "np_u_fk": { + "name": "np_u_fk", + "tableFrom": "notification_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_preferences_id": { + "name": "notification_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "notification_preferences_user_id_unique": { + "name": "notification_preferences_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "parent_student_relations": { + "name": "parent_student_relations", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "relation": { + "name": "relation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "parent_student_relations_parent_idx": { + "name": "parent_student_relations_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "parent_student_relations_student_idx": { + "name": "parent_student_relations_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "parent_student_relations_parent_id_users_id_fk": { + "name": "parent_student_relations_parent_id_users_id_fk", + "tableFrom": "parent_student_relations", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parent_student_relations_student_id_users_id_fk": { + "name": "parent_student_relations_student_id_users_id_fk", + "tableFrom": "parent_student_relations", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "psr_p_fk": { + "name": "psr_p_fk", + "tableFrom": "parent_student_relations", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "psr_s_fk": { + "name": "psr_s_fk", + "tableFrom": "parent_student_relations", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parent_student_relations_id": { + "name": "parent_student_relations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "password_security": { + "name": "password_security", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "failed_login_attempts": { + "name": "failed_login_attempts", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_changed_at": { + "name": "password_changed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "must_change_password": { + "name": "must_change_password", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_password_change": { + "name": "last_password_change", + "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": { + "password_security_user_idx": { + "name": "password_security_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "password_security_user_id_users_id_fk": { + "name": "password_security_user_id_users_id_fk", + "tableFrom": "password_security", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ps_u_fk": { + "name": "ps_u_fk", + "tableFrom": "password_security", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "password_security_id": { + "name": "password_security_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "password_security_user_id_unique": { + "name": "password_security_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "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": {} + }, + "role_permissions": { + "name": "role_permissions", + "columns": { + "role_id": { + "name": "role_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "role_permissions_role_idx": { + "name": "role_permissions_role_idx", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_permissions_role_id_permission_pk": { + "name": "role_permissions_role_id_permission_pk", + "columns": [ + "role_id", + "permission" + ] + } + }, + "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": {} + }, + "schedule_changes": { + "name": "schedule_changes", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_schedule_id": { + "name": "original_schedule_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_teacher_id": { + "name": "original_teacher_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "substitute_teacher_id": { + "name": "substitute_teacher_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "original_date": { + "name": "original_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_date": { + "name": "new_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_start_time": { + "name": "new_start_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_end_time": { + "name": "new_end_time", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','approved','rejected','completed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "requested_by": { + "name": "requested_by", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approved_by": { + "name": "approved_by", + "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": { + "schedule_changes_class_idx": { + "name": "schedule_changes_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "schedule_changes_status_idx": { + "name": "schedule_changes_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "schedule_changes_requested_by_idx": { + "name": "schedule_changes_requested_by_idx", + "columns": [ + "requested_by" + ], + "isUnique": false + }, + "schedule_changes_original_schedule_idx": { + "name": "schedule_changes_original_schedule_idx", + "columns": [ + "original_schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "schedule_changes_id": { + "name": "schedule_changes_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "scheduling_rules": { + "name": "scheduling_rules", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_daily_hours": { + "name": "max_daily_hours", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 8 + }, + "max_continuous_hours": { + "name": "max_continuous_hours", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2 + }, + "lunch_break_start": { + "name": "lunch_break_start", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'12:00'" + }, + "lunch_break_end": { + "name": "lunch_break_end", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'13:00'" + }, + "morning_start": { + "name": "morning_start", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'08:00'" + }, + "afternoon_end": { + "name": "afternoon_end", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'17:00'" + }, + "avoid_back_to_back": { + "name": "avoid_back_to_back", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "balanced_subjects": { + "name": "balanced_subjects", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "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": { + "scheduling_rules_class_idx": { + "name": "scheduling_rules_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scheduling_rules_id": { + "name": "scheduling_rules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "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": {} + }, + "system_settings": { + "name": "system_settings", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_type": { + "name": "value_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'string'" + }, + "updated_by": { + "name": "updated_by", + "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": { + "ss_category_key_idx": { + "name": "ss_category_key_idx", + "columns": [ + "category", + "key" + ], + "isUnique": true + }, + "ss_category_idx": { + "name": "ss_category_idx", + "columns": [ + "category" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "system_settings_id": { + "name": "system_settings_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 + }, + "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 + }, + "birth_date": { + "name": "birth_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guardian_name": { + "name": "guardian_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guardian_phone": { + "name": "guardian_phone", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guardian_relation": { + "name": "guardian_relation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "consent_accepted_at": { + "name": "consent_accepted_at", + "type": "datetime", + "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/src/app/(dashboard)/management/grade/insights/page.tsx b/src/app/(dashboard)/management/grade/insights/page.tsx index 68f9939..ecf4c68 100644 --- a/src/app/(dashboard)/management/grade/insights/page.tsx +++ b/src/app/(dashboard)/management/grade/insights/page.tsx @@ -1,3 +1,7 @@ +import type { Metadata } from "next" +import type { JSX } from "react" + +import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { getTeacherIdForMutations } from "@/modules/classes/data-access" @@ -11,22 +15,23 @@ import { Button } from "@/shared/components/ui/button" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table" import { BarChart3 } from "lucide-react" import { formatDate } from "@/shared/lib/utils" +import { getParam, type SearchParams } from "@/shared/lib/search-params" export const dynamic = "force-dynamic" -type SearchParams = { [key: string]: string | string[] | undefined } - -const getParam = (params: SearchParams, key: string) => { - const v = params[key] - if (typeof v === "string") return v - if (Array.isArray(v)) return v[0] - return undefined -} - const formatScore = (v: number | null, digits = 1) => (typeof v === "number" && Number.isFinite(v) ? v.toFixed(digits) : "-") -export default async function TeacherGradeInsightsPage({ searchParams }: { searchParams: Promise }) { +export async function generateMetadata(): Promise { + const t = await getTranslations("school") + return { + title: `${t("classManagement.grade.insights.title")} - Next_Edu`, + description: t("classManagement.grade.insights.description"), + } +} + +export default async function TeacherGradeInsightsPage({ searchParams }: { searchParams: Promise }): Promise { await requirePermission(Permissions.GRADE_RECORD_READ) + const t = await getTranslations("school") const params = await searchParams const gradeId = getParam(params, "gradeId") @@ -41,13 +46,13 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc return (
-

Grade Insights

-

View grade-level homework statistics for grades you lead.

+

{t("classManagement.grade.insights.title")}

+

{t("classManagement.grade.insights.description")}

@@ -57,27 +62,27 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc return (
-

Grade Insights

-

Homework statistics aggregated across all classes in a grade.

+

{t("classManagement.grade.insights.title")}

+

{t("classManagement.grade.insights.description")}

- Filters + {t("classManagement.grade.insights.filters")} {grades.length}
- +
@@ -94,47 +99,47 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc {!selected ? ( ) : !insights ? ( ) : insights.assignments.length === 0 ? ( ) : (
- Homework timeline + {t("classManagement.grade.insights.homeworkTimeline")} {insights.assignments.length} @@ -153,14 +158,14 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc - Assignment - Status - Created - Targeted - Submitted - Graded - Avg - Median + {t("classManagement.grade.insights.assignment")} + {t("classManagement.grade.insights.status")} + {t("classManagement.grade.insights.created")} + {t("classManagement.grade.insights.targeted")} + {t("classManagement.grade.insights.submitted")} + {t("classManagement.grade.insights.graded")} + {t("classManagement.grade.insights.avg")} + {t("classManagement.grade.insights.median")} @@ -188,7 +193,7 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc - Class ranking + {t("classManagement.grade.insights.classRanking")} {insights.classes.length} @@ -198,12 +203,12 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
- Class - Students - Latest Avg - Prev Avg - Δ - Overall Avg + {t("classManagement.grade.insights.class")} + {t("classManagement.grade.insights.students")} + {t("classManagement.grade.insights.latestAvgCol")} + {t("classManagement.grade.insights.prevAvg")} + {t("classManagement.grade.insights.delta")} + {t("classManagement.grade.insights.overallAvgCol")} @@ -230,4 +235,3 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc ) } - diff --git a/src/app/(dashboard)/parent/diagnostic/error.tsx b/src/app/(dashboard)/parent/diagnostic/error.tsx new file mode 100644 index 0000000..b195dd9 --- /dev/null +++ b/src/app/(dashboard)/parent/diagnostic/error.tsx @@ -0,0 +1,27 @@ +"use client" + +import { AlertCircle } from "lucide-react" + +import { EmptyState } from "@/shared/components/ui/empty-state" + +export default function ParentDiagnosticError({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+ reset(), + }} + className="border-none shadow-none h-auto" + /> +
+ ) +} diff --git a/src/app/(dashboard)/parent/diagnostic/loading.tsx b/src/app/(dashboard)/parent/diagnostic/loading.tsx new file mode 100644 index 0000000..8afefdc --- /dev/null +++ b/src/app/(dashboard)/parent/diagnostic/loading.tsx @@ -0,0 +1,32 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { Skeleton } from "@/shared/components/ui/skeleton" + +export default function Loading() { + return ( +
+
+ + +
+
+ {Array.from({ length: 2 }).map((_, i) => ( + + + + + + + + +
+ {Array.from({ length: 2 }).map((_, j) => ( + + ))} +
+
+
+ ))} +
+
+ ) +} diff --git a/src/app/(dashboard)/parent/diagnostic/page.tsx b/src/app/(dashboard)/parent/diagnostic/page.tsx new file mode 100644 index 0000000..5772871 --- /dev/null +++ b/src/app/(dashboard)/parent/diagnostic/page.tsx @@ -0,0 +1,70 @@ +import { Stethoscope } from "lucide-react" + +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" +import { getStudentMasterySummary } from "@/modules/diagnostic/data-access" +import { getDiagnosticReports } from "@/modules/diagnostic/data-access-reports" +import { StudentDiagnosticView } from "@/modules/diagnostic/components/student-diagnostic-view" +import { + ParentChildrenDataPage, + ParentNoChildrenPage, +} from "@/modules/parent/components/parent-children-data-page" + +export const dynamic = "force-dynamic" + +export default async function ParentDiagnosticPage() { + const ctx = await requirePermission(Permissions.DIAGNOSTIC_READ) + + if (ctx.dataScope.type !== "children" || ctx.dataScope.childrenIds.length === 0) { + return ( + + ) + } + + // 使用 allSettled 容错:单个子女查询失败不影响其他子女展示 + const results = await Promise.allSettled( + ctx.dataScope.childrenIds.map(async (id) => { + const [summary, reports] = await Promise.all([ + getStudentMasterySummary(id), + getDiagnosticReports({ studentId: id }), + ]) + return { summary, reports, studentId: id } + }), + ) + const validItems = results + .filter( + (r): r is PromiseFulfilledResult<{ + summary: Awaited> + reports: Awaited> + studentId: string + }> => r.status === "fulfilled", + ) + .map((r) => r.value) + + return ( + ( + <> +
+

+ {summary?.studentName ?? "Unknown student"} +

+
+ + + )} + /> + ) +} diff --git a/src/app/(dashboard)/student/grades/page.tsx b/src/app/(dashboard)/student/grades/page.tsx index 1e0f02b..812abcc 100644 --- a/src/app/(dashboard)/student/grades/page.tsx +++ b/src/app/(dashboard)/student/grades/page.tsx @@ -1,26 +1,21 @@ -import { getAuthContext } from "@/shared/lib/auth-guard" +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" import { getStudentGradeSummary } from "@/modules/grades/data-access" import { StudentGradeSummary } from "@/modules/grades/components/student-grade-summary" import { GradeFilters } from "@/modules/grades/components/grade-filters" import { GradeTrendCard } from "@/modules/grades/components/grade-trend-card" import { EmptyState } from "@/shared/components/ui/empty-state" import { UserX } from "lucide-react" +import { getParam, type SearchParams } from "@/shared/lib/search-params" export const dynamic = "force-dynamic" -type SearchParams = { [key: string]: string | string[] | undefined } - -const getParam = (params: SearchParams, key: string) => { - const v = params[key] - return Array.isArray(v) ? v[0] : v -} - export default async function StudentGradesPage({ searchParams, }: { searchParams: Promise }) { - const ctx = await getAuthContext() + const ctx = await requirePermission(Permissions.GRADE_RECORD_READ) const [sp, summary] = await Promise.all([ searchParams, getStudentGradeSummary(ctx.userId), diff --git a/src/app/(dashboard)/teacher/diagnostic/class/[classId]/error.tsx b/src/app/(dashboard)/teacher/diagnostic/class/[classId]/error.tsx new file mode 100644 index 0000000..af5f442 --- /dev/null +++ b/src/app/(dashboard)/teacher/diagnostic/class/[classId]/error.tsx @@ -0,0 +1,27 @@ +"use client" + +import { AlertCircle } from "lucide-react" + +import { EmptyState } from "@/shared/components/ui/empty-state" + +export default function DiagnosticClassError({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+ reset(), + }} + className="border-none shadow-none h-auto" + /> +
+ ) +} diff --git a/src/app/(dashboard)/teacher/diagnostic/class/[classId]/loading.tsx b/src/app/(dashboard)/teacher/diagnostic/class/[classId]/loading.tsx new file mode 100644 index 0000000..0ae5d06 --- /dev/null +++ b/src/app/(dashboard)/teacher/diagnostic/class/[classId]/loading.tsx @@ -0,0 +1,30 @@ +import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" +import { Skeleton } from "@/shared/components/ui/skeleton" + +export default function Loading() { + return ( +
+ +
+ + + + + + + + + + + + + + {Array.from({ length: 6 }).map((_, i) => ( + + ))} + + +
+
+ ) +} diff --git a/src/app/(dashboard)/teacher/diagnostic/error.tsx b/src/app/(dashboard)/teacher/diagnostic/error.tsx new file mode 100644 index 0000000..0849608 --- /dev/null +++ b/src/app/(dashboard)/teacher/diagnostic/error.tsx @@ -0,0 +1,27 @@ +"use client" + +import { AlertCircle } from "lucide-react" + +import { EmptyState } from "@/shared/components/ui/empty-state" + +export default function TeacherDiagnosticError({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+ reset(), + }} + className="border-none shadow-none h-auto" + /> +
+ ) +} diff --git a/src/app/(dashboard)/teacher/diagnostic/loading.tsx b/src/app/(dashboard)/teacher/diagnostic/loading.tsx new file mode 100644 index 0000000..142ce84 --- /dev/null +++ b/src/app/(dashboard)/teacher/diagnostic/loading.tsx @@ -0,0 +1,23 @@ +import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" +import { Skeleton } from "@/shared/components/ui/skeleton" + +export default function Loading() { + return ( +
+
+ + +
+ + + + + + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + +
+ ) +} diff --git a/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/error.tsx b/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/error.tsx new file mode 100644 index 0000000..cc75dae --- /dev/null +++ b/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/error.tsx @@ -0,0 +1,27 @@ +"use client" + +import { AlertCircle } from "lucide-react" + +import { EmptyState } from "@/shared/components/ui/empty-state" + +export default function DiagnosticStudentError({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+ reset(), + }} + className="border-none shadow-none h-auto" + /> +
+ ) +} diff --git a/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/loading.tsx b/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/loading.tsx new file mode 100644 index 0000000..0ae5d06 --- /dev/null +++ b/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/loading.tsx @@ -0,0 +1,30 @@ +import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" +import { Skeleton } from "@/shared/components/ui/skeleton" + +export default function Loading() { + return ( +
+ +
+ + + + + + + + + + + + + + {Array.from({ length: 6 }).map((_, i) => ( + + ))} + + +
+
+ ) +} diff --git a/src/app/(dashboard)/teacher/grades/analytics/error.tsx b/src/app/(dashboard)/teacher/grades/analytics/error.tsx new file mode 100644 index 0000000..ecc3756 --- /dev/null +++ b/src/app/(dashboard)/teacher/grades/analytics/error.tsx @@ -0,0 +1,27 @@ +"use client" + +import { AlertCircle } from "lucide-react" + +import { EmptyState } from "@/shared/components/ui/empty-state" + +export default function TeacherGradesAnalyticsError({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+ reset(), + }} + className="border-none shadow-none h-auto" + /> +
+ ) +} diff --git a/src/app/(dashboard)/teacher/grades/analytics/loading.tsx b/src/app/(dashboard)/teacher/grades/analytics/loading.tsx new file mode 100644 index 0000000..a07cef9 --- /dev/null +++ b/src/app/(dashboard)/teacher/grades/analytics/loading.tsx @@ -0,0 +1,31 @@ +import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" +import { Skeleton } from "@/shared/components/ui/skeleton" + +export default function Loading() { + return ( +
+
+ + +
+
+ + + + + + + + + + + + + + + + +
+
+ ) +} diff --git a/src/app/(dashboard)/teacher/grades/entry/error.tsx b/src/app/(dashboard)/teacher/grades/entry/error.tsx new file mode 100644 index 0000000..181823d --- /dev/null +++ b/src/app/(dashboard)/teacher/grades/entry/error.tsx @@ -0,0 +1,27 @@ +"use client" + +import { AlertCircle } from "lucide-react" + +import { EmptyState } from "@/shared/components/ui/empty-state" + +export default function TeacherGradesEntryError({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+ reset(), + }} + className="border-none shadow-none h-auto" + /> +
+ ) +} diff --git a/src/app/(dashboard)/teacher/grades/entry/loading.tsx b/src/app/(dashboard)/teacher/grades/entry/loading.tsx new file mode 100644 index 0000000..db96284 --- /dev/null +++ b/src/app/(dashboard)/teacher/grades/entry/loading.tsx @@ -0,0 +1,23 @@ +import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" +import { Skeleton } from "@/shared/components/ui/skeleton" + +export default function Loading() { + return ( +
+
+ + +
+ + + + + + {Array.from({ length: 8 }).map((_, i) => ( + + ))} + + +
+ ) +} diff --git a/src/app/(dashboard)/teacher/grades/error.tsx b/src/app/(dashboard)/teacher/grades/error.tsx new file mode 100644 index 0000000..7c3d50c --- /dev/null +++ b/src/app/(dashboard)/teacher/grades/error.tsx @@ -0,0 +1,27 @@ +"use client" + +import { AlertCircle } from "lucide-react" + +import { EmptyState } from "@/shared/components/ui/empty-state" + +export default function TeacherGradesError({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+ reset(), + }} + className="border-none shadow-none h-auto" + /> +
+ ) +} diff --git a/src/app/(dashboard)/teacher/grades/loading.tsx b/src/app/(dashboard)/teacher/grades/loading.tsx new file mode 100644 index 0000000..889c68b --- /dev/null +++ b/src/app/(dashboard)/teacher/grades/loading.tsx @@ -0,0 +1,28 @@ +import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" +import { Skeleton } from "@/shared/components/ui/skeleton" + +export default function Loading() { + return ( +
+
+ + +
+
+ + + +
+ + + + + + {Array.from({ length: 6 }).map((_, i) => ( + + ))} + + +
+ ) +} diff --git a/src/app/(dashboard)/teacher/grades/stats/error.tsx b/src/app/(dashboard)/teacher/grades/stats/error.tsx new file mode 100644 index 0000000..afc6697 --- /dev/null +++ b/src/app/(dashboard)/teacher/grades/stats/error.tsx @@ -0,0 +1,27 @@ +"use client" + +import { AlertCircle } from "lucide-react" + +import { EmptyState } from "@/shared/components/ui/empty-state" + +export default function TeacherGradesStatsError({ + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+ reset(), + }} + className="border-none shadow-none h-auto" + /> +
+ ) +} diff --git a/src/app/(dashboard)/teacher/grades/stats/loading.tsx b/src/app/(dashboard)/teacher/grades/stats/loading.tsx new file mode 100644 index 0000000..5976064 --- /dev/null +++ b/src/app/(dashboard)/teacher/grades/stats/loading.tsx @@ -0,0 +1,33 @@ +import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" +import { Skeleton } from "@/shared/components/ui/skeleton" + +export default function Loading() { + return ( +
+
+ + +
+
+ {Array.from({ length: 3 }).map((_, i) => ( + + + + + + + + + ))} +
+ + + + + + + + +
+ ) +} diff --git a/src/modules/diagnostic/components/mastery-radar-chart.tsx b/src/modules/diagnostic/components/mastery-radar-chart.tsx index 6267246..d550476 100644 --- a/src/modules/diagnostic/components/mastery-radar-chart.tsx +++ b/src/modules/diagnostic/components/mastery-radar-chart.tsx @@ -35,35 +35,37 @@ export function MasteryRadarChart({ data }: MasteryRadarChartProps) { emptyDescription="No knowledge point mastery records found for this student." emptyClassName="h-60" > - +
+ +
) } diff --git a/src/modules/diagnostic/components/report-list.tsx b/src/modules/diagnostic/components/report-list.tsx index e067f9b..bf78763 100644 --- a/src/modules/diagnostic/components/report-list.tsx +++ b/src/modules/diagnostic/components/report-list.tsx @@ -157,6 +157,7 @@ export function ReportList({ reports }: ReportListProps) { ) : (
+ Type @@ -175,7 +176,7 @@ export function ReportList({ reports }: ReportListProps) { {typeLabels[r.reportType] ?? r.reportType} - {r.studentName} + {r.studentName ?? (r.reportType === "class" ? "(班级报告)" : r.reportType === "grade" ? "(年级报告)" : "-")} {r.period ?? "-"} {r.overallScore !== null ? `${r.overallScore.toFixed(1)}%` : "-"} @@ -195,6 +196,7 @@ export function ReportList({ reports }: ReportListProps) { className="h-8 w-8 text-green-600" onClick={() => setPublishId(r.id)} title="Publish" + aria-label={`发布报告 ${r.studentName}`} > @@ -205,6 +207,7 @@ export function ReportList({ reports }: ReportListProps) { className="h-8 w-8 text-destructive" onClick={() => setDeleteId(r.id)} title="Delete" + aria-label={`删除报告 ${r.studentName}`} > diff --git a/src/modules/diagnostic/components/student-diagnostic-view.tsx b/src/modules/diagnostic/components/student-diagnostic-view.tsx index 5eb61a1..8f7b447 100644 --- a/src/modules/diagnostic/components/student-diagnostic-view.tsx +++ b/src/modules/diagnostic/components/student-diagnostic-view.tsx @@ -96,7 +96,7 @@ export function StudentDiagnosticView({ summary, reports, classAverageMastery }: {summary.strengths.length === 0 ? (

No strengths identified yet.

) : ( -
    +
      {summary.strengths.map((m) => (
    • {m.knowledgePointName} @@ -119,7 +119,7 @@ export function StudentDiagnosticView({ summary, reports, classAverageMastery }: {summary.weaknesses.length === 0 ? (

      No weaknesses identified.

      ) : ( -
        +
          {summary.weaknesses.map((m) => (
        • @@ -162,7 +162,7 @@ export function StudentDiagnosticView({ summary, reports, classAverageMastery }: {latestReport.recommendations && latestReport.recommendations.length > 0 ? (

          Recommendations

          -
            +
              {latestReport.recommendations.map((rec, i) => (
            • • {rec}
            • ))} diff --git a/src/modules/diagnostic/data-access-reports.ts b/src/modules/diagnostic/data-access-reports.ts index e048f66..b54fa62 100644 --- a/src/modules/diagnostic/data-access-reports.ts +++ b/src/modules/diagnostic/data-access-reports.ts @@ -109,7 +109,7 @@ export async function generateClassDiagnosticReport( const id = createId() await db.insert(learningDiagnosticReports).values({ id, - studentId: generatedBy, // 班级报告 studentId 存生成者 ID(schema 要求 NOT NULL) + studentId: null, // 班级报告无单个学生,studentId 置空(P2-3 修复:不再存生成者 ID) generatedBy, reportType: "class", period, @@ -141,14 +141,16 @@ export const getDiagnosticReports = cache( // 收集所有需要查询姓名的用户 ID(学生 + 生成者),通过 users data-access 统一获取 const userIds = new Set() for (const r of rows) { - userIds.add(r.report.studentId) + if (r.report.studentId) userIds.add(r.report.studentId) if (r.report.generatedBy) userIds.add(r.report.generatedBy) } const userMap = await getUserNamesByIds(Array.from(userIds)) return rows.map((r) => ({ ...serializeReport(r.report), - studentName: userMap.get(r.report.studentId)?.name ?? "Unknown", + studentName: r.report.studentId + ? userMap.get(r.report.studentId)?.name ?? "Unknown" + : null, generatedByName: r.report.generatedBy ? userMap.get(r.report.generatedBy)?.name ?? "Unknown" : null, @@ -167,13 +169,16 @@ export const getDiagnosticReportById = cache( if (!row) return null // 通过 users data-access 获取学生姓名和生成者姓名 - const userIds = [row.report.studentId] + const userIds: string[] = [] + if (row.report.studentId) userIds.push(row.report.studentId) if (row.report.generatedBy) userIds.push(row.report.generatedBy) const userMap = await getUserNamesByIds(userIds) return { ...serializeReport(row.report), - studentName: userMap.get(row.report.studentId)?.name ?? "Unknown", + studentName: row.report.studentId + ? userMap.get(row.report.studentId)?.name ?? "Unknown" + : null, generatedByName: row.report.generatedBy ? userMap.get(row.report.generatedBy)?.name ?? null : null, diff --git a/src/modules/diagnostic/types.ts b/src/modules/diagnostic/types.ts index b848314..555648b 100644 --- a/src/modules/diagnostic/types.ts +++ b/src/modules/diagnostic/types.ts @@ -36,7 +36,7 @@ export interface StudentMasterySummary { /** 诊断报告 */ export interface DiagnosticReport { id: string - studentId: string + studentId: string | null generatedBy: string | null reportType: DiagnosticReportType period: string | null @@ -52,7 +52,7 @@ export interface DiagnosticReport { /** 含学生名的诊断报告(join users 后) */ export interface DiagnosticReportWithDetails extends DiagnosticReport { - studentName: string + studentName: string | null generatedByName: string | null } diff --git a/src/modules/grades/components/class-comparison-chart.tsx b/src/modules/grades/components/class-comparison-chart.tsx index 228316b..1ac7a87 100644 --- a/src/modules/grades/components/class-comparison-chart.tsx +++ b/src/modules/grades/components/class-comparison-chart.tsx @@ -38,23 +38,25 @@ export function ClassComparisonChart({ data }: ClassComparisonChartProps) { emptyDescription="Select a grade and subject to compare classes." emptyClassName="h-60" > - `${value}%`} - yWidth={36} - heightClassName="h-[300px]" - margin={{ left: 8, right: 8, top: 8, bottom: 8 }} - showLegend - tooltipClassName="w-[240px]" - /> +
              + `${value}%`} + yWidth={36} + heightClassName="h-[300px]" + margin={{ left: 8, right: 8, top: 8, bottom: 8 }} + showLegend + tooltipClassName="w-[240px]" + /> +
              ) } diff --git a/src/modules/grades/components/grade-distribution-chart.tsx b/src/modules/grades/components/grade-distribution-chart.tsx index 852bc80..b98aee4 100644 --- a/src/modules/grades/components/grade-distribution-chart.tsx +++ b/src/modules/grades/components/grade-distribution-chart.tsx @@ -70,37 +70,39 @@ export function GradeDistributionChart({ data }: GradeDistributionChartProps) { emptyDescription="Select a class and subject to view score distribution." emptyClassName="h-60" > - { - if (!isDistributionTooltipPayload(payload)) return null - const item = payload.payload - if (!item) return null - return ( -
              - - {item.label}: {item.count} student{item.count === 1 ? "" : "s"} - - {item.percentage}% of total -
              - ) - }} - /> +
              + { + if (!isDistributionTooltipPayload(payload)) return null + const item = payload.payload + if (!item) return null + return ( +
              + + {item.label}: {item.count} student{item.count === 1 ? "" : "s"} + + {item.percentage}% of total +
              + ) + }} + /> +
              ) } diff --git a/src/modules/grades/components/grade-record-list.tsx b/src/modules/grades/components/grade-record-list.tsx index c216ecf..3df6fac 100644 --- a/src/modules/grades/components/grade-record-list.tsx +++ b/src/modules/grades/components/grade-record-list.tsx @@ -3,7 +3,6 @@ import { useState } from "react" import { toast } from "sonner" import { useRouter } from "next/navigation" -import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" import { Table, @@ -21,18 +20,13 @@ import { DialogHeader, DialogTitle, } from "@/shared/components/ui/dialog" +import { StatusBadge } from "@/shared/components/ui/status-badge" import { formatDate } from "@/shared/lib/utils" import { Trash2 } from "lucide-react" import { deleteGradeRecordAction } from "../actions" import type { GradeRecordListItem } from "../types" - -const typeColors: Record = { - exam: "default", - quiz: "secondary", - homework: "outline", - other: "outline", -} +import { GRADE_TYPE_VARIANT } from "../types" export function GradeRecordList({ records }: { records: GradeRecordListItem[] }) { const router = useRouter() @@ -65,6 +59,7 @@ export function GradeRecordList({ records }: { records: GradeRecordListItem[] }) <>
学情诊断报告列表
+ Student @@ -90,9 +85,7 @@ export function GradeRecordList({ records }: { records: GradeRecordListItem[] }) {r.score} / {r.fullScore} - - {r.type} - + S{r.semester} {r.recorderName} @@ -103,6 +96,7 @@ export function GradeRecordList({ records }: { records: GradeRecordListItem[] }) size="icon" className="h-8 w-8 text-destructive" onClick={() => setDeleteId(r.id)} + aria-label={`删除 ${r.studentName} 的 ${r.subjectName} 成绩记录`} > diff --git a/src/modules/grades/components/grade-trend-chart.tsx b/src/modules/grades/components/grade-trend-chart.tsx index 9242776..b5b58e3 100644 --- a/src/modules/grades/components/grade-trend-chart.tsx +++ b/src/modules/grades/components/grade-trend-chart.tsx @@ -40,22 +40,24 @@ export function GradeTrendChart({ data }: GradeTrendChartProps) { emptyDescription="Select a class and subject to view the grade trend." emptyClassName="h-60" > - +
+ +
) } diff --git a/src/modules/grades/components/subject-comparison-chart.tsx b/src/modules/grades/components/subject-comparison-chart.tsx index bdace11..780b505 100644 --- a/src/modules/grades/components/subject-comparison-chart.tsx +++ b/src/modules/grades/components/subject-comparison-chart.tsx @@ -37,28 +37,30 @@ export function SubjectComparisonChart({ data }: SubjectComparisonChartProps) { emptyDescription="Select a class to compare subject performance." emptyClassName="h-60" > - - value.length > 6 ? `${value.slice(0, 6)}...` : value - } - heightClassName="h-[300px]" - series={[ - { - dataKey: "averageScore", - name: "Average", - color: "hsl(var(--primary))", - fillOpacity: 0.4, - }, - { - dataKey: "passRate", - name: "Pass Rate", - color: "hsl(var(--chart-2))", - fillOpacity: 0.2, - }, - ]} - /> +
+ + value.length > 6 ? `${value.slice(0, 6)}...` : value + } + heightClassName="h-[300px]" + series={[ + { + dataKey: "averageScore", + name: "Average", + color: "hsl(var(--primary))", + fillOpacity: 0.4, + }, + { + dataKey: "passRate", + name: "Pass Rate", + color: "hsl(var(--chart-2))", + fillOpacity: 0.2, + }, + ]} + /> +
) } diff --git a/src/modules/grades/components/widget-boundary.tsx b/src/modules/grades/components/widget-boundary.tsx new file mode 100644 index 0000000..ca66d81 --- /dev/null +++ b/src/modules/grades/components/widget-boundary.tsx @@ -0,0 +1,139 @@ +"use client" + +/** + * Grades/Diagnostic 模块通用 Widget 边界组件。 + * + * 组合三个能力: + * 1. Error Boundary — 隔离故障域,单个 Widget 抛错不影响其他区块 + * 2. Suspense — 流式渲染时显示骨架屏,避免白屏等待 + * 3. Skeleton — 与 Widget 尺寸匹配的占位 + * + * 用法: + * ```tsx + * + * + * + * ``` + */ + +import { Component, Suspense, type ReactNode } from "react" +import { AlertCircle } from "lucide-react" +import { Button } from "@/shared/components/ui/button" +import { Skeleton } from "@/shared/components/ui/skeleton" + +interface WidgetBoundaryProps { + children: ReactNode + /** Widget 标题(用于错误提示和 aria-label) */ + title?: string + /** 骨架屏高度(默认 200px) */ + skeletonHeight?: number + /** 自定义错误描述 */ + fallbackDescription?: string + /** 重试按钮文案 */ + retryLabel?: string +} + +interface WidgetBoundaryState { + hasError: boolean +} + +class WidgetErrorBoundary extends Component< + Pick< + WidgetBoundaryProps, + "title" | "fallbackDescription" | "retryLabel" | "children" + >, + WidgetBoundaryState +> { + constructor(props: WidgetBoundaryProps) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(): WidgetBoundaryState { + return { hasError: true } + } + + handleReset = (): void => { + this.setState({ hasError: false }) + } + + render(): ReactNode { + if (this.state.hasError) { + const title = this.props.title ?? "区块" + return ( +
+
+ ) + } + + return this.props.children + } +} + +function WidgetSkeleton({ + height, + title, +}: { + height: number + title?: string +}): ReactNode { + return ( +
+ + + + +
+ ) +} + +export function WidgetBoundary({ + children, + title, + skeletonHeight = 200, + fallbackDescription, + retryLabel, +}: WidgetBoundaryProps): ReactNode { + return ( + + + } + > + {children} + + + ) +} diff --git a/src/modules/grades/data-access-analytics.ts b/src/modules/grades/data-access-analytics.ts index 9d9773f..8808998 100644 --- a/src/modules/grades/data-access-analytics.ts +++ b/src/modules/grades/data-access-analytics.ts @@ -13,11 +13,16 @@ import { getSubjectOptions } from "@/modules/school/data-access" import type { DataScope } from "@/shared/types/permissions" import { buildScopeClassFilter, normalize, toNumber } from "./lib/grade-utils" +import { + buildGradeTrendPoints, + computeClassComparisonStats, + computeGradeDistribution, + computeSubjectComparisonStats, + computeTrendAverage, +} from "./stats-service" import type { ClassComparisonItem, - GradeDistributionBucket, GradeDistributionResult, - GradeTrendPoint, GradeTrendResult, SubjectComparisonItem, } from "./types" @@ -64,20 +69,8 @@ export const getGradeTrend = cache( subjectName = subject?.name ?? "Unknown" } - const points: GradeTrendPoint[] = rows.map((r) => { - const score = toNumber(r.record.score) - const fullScore = toNumber(r.record.fullScore) - return { - date: r.record.createdAt.toISOString(), - title: r.record.title, - score, - fullScore, - normalizedScore: normalize(score, fullScore), - type: r.record.type, - } - }) - - const avg = points.reduce((acc, p) => acc + p.normalizedScore, 0) / points.length + const points = buildGradeTrendPoints(rows) + const avg = computeTrendAverage(points) const finalClassName = className ?? "Class" const studentLabel = params.studentId ? `Student ${params.studentId.slice(-4)}` @@ -88,7 +81,7 @@ export const getGradeTrend = cache( ? `${finalClassName} · ${subjectName} · ${studentLabel}` : `${finalClassName} · ${studentLabel}`, points, - averageScore: Math.round(avg * 100) / 100, + averageScore: avg, } } ) @@ -145,46 +138,11 @@ export const getClassComparison = cache( const result: ClassComparisonItem[] = allowedClassRows.map((cls) => { const rows = byClass.get(cls.id) ?? [] - if (rows.length === 0) { - return { - classId: cls.id, - className: cls.name, - averageScore: 0, - medianScore: 0, - passRate: 0, - excellentRate: 0, - count: 0, - studentCount: 0, - } - } - - const normalized = rows.map((r) => - normalize(toNumber(r.score), toNumber(r.fullScore)) - ) - const sorted = [...normalized].sort((a, b) => a - b) - const mid = Math.floor(sorted.length / 2) - const median = - sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid] - const avg = normalized.reduce((a, b) => a + b, 0) / normalized.length - const uniqueStudents = new Set(rows.map((r) => r.studentId)).size - - const { passCount, excellentCount } = normalized.reduce( - (acc, s) => ({ - passCount: acc.passCount + (s >= 60 ? 1 : 0), - excellentCount: acc.excellentCount + (s >= 85 ? 1 : 0), - }), - { passCount: 0, excellentCount: 0 } - ) - + const stats = computeClassComparisonStats(rows) return { classId: cls.id, className: cls.name, - averageScore: Math.round(avg * 100) / 100, - medianScore: Math.round(median * 100) / 100, - passRate: Math.round((passCount / normalized.length) * 10000) / 100, - excellentRate: Math.round((excellentCount / normalized.length) * 10000) / 100, - count: normalized.length, - studentCount: uniqueStudents, + ...stats, } }) @@ -234,28 +192,11 @@ export const getSubjectComparison = cache( const result: SubjectComparisonItem[] = [] for (const [subjectId, entry] of bySubject.entries()) { if (entry.scores.length === 0) continue - const sorted = [...entry.scores].sort((a, b) => a - b) - const mid = Math.floor(sorted.length / 2) - const median = - sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid] - const avg = entry.scores.reduce((a, b) => a + b, 0) / entry.scores.length - - const { passCount, excellentCount } = entry.scores.reduce( - (acc, s) => ({ - passCount: acc.passCount + (s >= 60 ? 1 : 0), - excellentCount: acc.excellentCount + (s >= 85 ? 1 : 0), - }), - { passCount: 0, excellentCount: 0 } - ) - + const stats = computeSubjectComparisonStats(entry.scores) result.push({ subjectId, subjectName: entry.name, - averageScore: Math.round(avg * 100) / 100, - medianScore: Math.round(median * 100) / 100, - passRate: Math.round((passCount / entry.scores.length) * 10000) / 100, - excellentRate: Math.round((excellentCount / entry.scores.length) * 10000) / 100, - count: entry.scores.length, + ...stats, }) } @@ -289,24 +230,6 @@ export const getGradeDistribution = cache( .from(gradeRecords) .where(and(...conditions)) - const buckets: GradeDistributionBucket[] = [ - { label: "90-100", min: 90, max: 100, count: 0 }, - { label: "80-89", min: 80, max: 89, count: 0 }, - { label: "70-79", min: 70, max: 79, count: 0 }, - { label: "60-69", min: 60, max: 69, count: 0 }, - { label: "<60", min: 0, max: 59, count: 0 }, - ] - - for (const r of rows) { - const normalized = normalize(toNumber(r.score), toNumber(r.fullScore)) - const rounded = Math.round(normalized) - if (rounded >= 90) buckets[0].count++ - else if (rounded >= 80) buckets[1].count++ - else if (rounded >= 70) buckets[2].count++ - else if (rounded >= 60) buckets[3].count++ - else buckets[4].count++ - } - - return { buckets, totalCount: rows.length } + return computeGradeDistribution(rows) } ) diff --git a/src/modules/grades/data-access-ranking.ts b/src/modules/grades/data-access-ranking.ts index 72099ca..dc4c341 100644 --- a/src/modules/grades/data-access-ranking.ts +++ b/src/modules/grades/data-access-ranking.ts @@ -9,8 +9,8 @@ import { getStudentActiveClassId } from "@/modules/classes/data-access" import { getUserNamesByIds } from "@/modules/users/data-access" import { normalize, toNumber } from "./lib/grade-utils" +import { buildRankingTrendPoints, type RankingTrendEntry } from "./stats-service" import type { - RankingTrendPoint, RankingTrendResult, } from "./types" @@ -56,13 +56,7 @@ export const getRankingTrend = cache( .where(and(...conditions)) .orderBy(asc(gradeRecords.createdAt)) - const byTitle = new Map< - string, - { - date: Date - entries: Array<{ studentId: string; normalized: number }> - } - >() + const byTitle = new Map() for (const r of rows) { const entry = byTitle.get(r.title) ?? { date: r.createdAt, entries: [] } @@ -73,33 +67,7 @@ export const getRankingTrend = cache( byTitle.set(r.title, entry) } - const points: RankingTrendPoint[] = [] - for (const [title, entry] of byTitle.entries()) { - if (entry.entries.length === 0) continue - const sorted = [...entry.entries].sort((a, b) => b.normalized - a.normalized) - // Single traversal: find rank and student entry together - let rank = 0 - let studentEntry: { studentId: string; normalized: number } | null = null - for (let i = 0; i < sorted.length; i += 1) { - const e = sorted[i] - if (e.studentId === studentId) { - rank = i + 1 - studentEntry = e - break - } - } - if (rank <= 0 || !studentEntry) continue - - points.push({ - title, - date: entry.date.toISOString(), - score: studentEntry.normalized, - rank, - totalStudents: sorted.length, - }) - } - - points.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) + const points = buildRankingTrendPoints(byTitle, studentId) return { studentId, diff --git a/src/modules/grades/data-access.ts b/src/modules/grades/data-access.ts index bf505f0..841b944 100644 --- a/src/modules/grades/data-access.ts +++ b/src/modules/grades/data-access.ts @@ -17,6 +17,7 @@ import { getUserNamesByIds } from "@/modules/users/data-access" import type { DataScope } from "@/shared/types/permissions" import { buildScopeClassFilter, toNumber } from "./lib/grade-utils" +import { computeAverageScore, computeGradeStats } from "./stats-service" import type { ClassGradeStats, ClassRankingItem, @@ -208,40 +209,7 @@ export const getClassGradeStats = cache( .from(gradeRecords) .where(and(...conditions)) - if (rows.length === 0) return null - - const scores = rows.map((r) => toNumber(r.score)) - const fullScores = rows.map((r) => toNumber(r.fullScore)) - const countN = scores.length - const sum = scores.reduce((a, b) => a + b, 0) - const average = sum / countN - const sorted = [...scores].sort((a, b) => a - b) - const mid = Math.floor(countN / 2) - const median = countN % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid] - const max = sorted[countN - 1] - const min = sorted[0] - const variance = scores.reduce((acc, s) => acc + Math.pow(s - average, 2), 0) / countN - const stdDev = Math.sqrt(variance) - - let passCount = 0 - let excellentCount = 0 - for (let i = 0; i < countN; i++) { - if (fullScores[i] <= 0) continue - const ratio = scores[i] / fullScores[i] - if (ratio >= 0.6) passCount++ - if (ratio >= 0.85) excellentCount++ - } - - return { - average: Math.round(average * 100) / 100, - median: Math.round(median * 100) / 100, - max, - min, - stdDev: Math.round(stdDev * 100) / 100, - passRate: Math.round((passCount / countN) * 10000) / 100, - excellentRate: Math.round((excellentCount / countN) * 10000) / 100, - count: countN, - } + return computeGradeStats(rows) } ) @@ -300,13 +268,13 @@ export const getStudentGradeSummary = cache( createdAt: r.record.createdAt.toISOString(), })) - const avg = listItems.reduce((a, b) => a + b.score, 0) / listItems.length + const avg = computeAverageScore(listItems.map((i) => i.score)) return { studentId, studentName: studentName ?? "Unknown", records: listItems, - averageScore: Math.round(avg * 100) / 100, + averageScore: avg, rank: 0, } } diff --git a/src/modules/grades/lib/grade-utils.ts b/src/modules/grades/lib/grade-utils.ts index 57426da..f08431f 100644 --- a/src/modules/grades/lib/grade-utils.ts +++ b/src/modules/grades/lib/grade-utils.ts @@ -2,7 +2,8 @@ import "server-only" import { eq, inArray, sql, type SQL } from "drizzle-orm" -import { gradeRecords } from "@/shared/db/schema" +import { db } from "@/shared/db" +import { classes, gradeRecords } from "@/shared/db/schema" import type { DataScope } from "@/shared/types/permissions" /** @@ -39,7 +40,16 @@ export const buildScopeClassFilter = (scope: DataScope): SQL | null => { ? inArray(gradeRecords.classId, scope.classIds) : sql`1=0` } - if (scope.type === "grade_managed") return sql`1=0` + if (scope.type === "grade_managed") { + // P2-4 修复:grade_managed scope 应返回所管年级的班级成绩记录 + // 通过子查询过滤 classId IN (SELECT id FROM classes WHERE grade_id IN (...)) + return scope.gradeIds.length > 0 + ? inArray( + gradeRecords.classId, + db.select({ id: classes.id }).from(classes).where(inArray(classes.gradeId, scope.gradeIds)) + ) + : sql`1=0` + } if (scope.type === "class_members") return null if (scope.type === "children") { return scope.childrenIds.length > 0 diff --git a/src/modules/grades/stats-service.ts b/src/modules/grades/stats-service.ts new file mode 100644 index 0000000..dcdf814 --- /dev/null +++ b/src/modules/grades/stats-service.ts @@ -0,0 +1,305 @@ +/** + * Grade statistics pure functions. + * + * Extracted from data-access / data-access-analytics / data-access-ranking + * to keep data-access layer focused on DB I/O and make statistics logic + * independently testable. All functions are pure (no side effects, no I/O). + */ + +import { normalize, toNumber } from "./lib/grade-utils" +import type { + ClassComparisonItem, + GradeDistributionBucket, + GradeDistributionResult, + GradeStats, + GradeTrendPoint, + RankingTrendPoint, +} from "./types" + +/** Round to 2 decimal places. */ +const round2 = (n: number): number => Math.round(n * 100) / 100 + +/** Pass threshold (60% of full score, normalized to 60/100). */ +export const PASS_THRESHOLD = 60 + +/** Excellent threshold (85% of full score, normalized to 85/100). */ +export const EXCELLENT_THRESHOLD = 85 + +/** + * Raw score row from DB (numeric columns may be string | number depending on driver). + */ +export interface RawScoreRow { + score: unknown + fullScore: unknown +} + +/** + * Compute aggregate stats (average/median/max/min/stdDev/passRate/excellentRate/count) + * from a list of raw score rows. Returns null when rows is empty. + */ +export function computeGradeStats(rows: RawScoreRow[]): GradeStats | null { + if (rows.length === 0) return null + + const scores = rows.map((r) => toNumber(r.score)) + const fullScores = rows.map((r) => toNumber(r.fullScore)) + const countN = scores.length + const sum = scores.reduce((a, b) => a + b, 0) + const average = sum / countN + const sorted = [...scores].sort((a, b) => a - b) + const mid = Math.floor(countN / 2) + const median = + countN % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid] + const max = sorted[countN - 1] + const min = sorted[0] + const variance = + scores.reduce((acc, s) => acc + Math.pow(s - average, 2), 0) / countN + const stdDev = Math.sqrt(variance) + + let passCount = 0 + let excellentCount = 0 + for (let i = 0; i < countN; i += 1) { + if (fullScores[i] <= 0) continue + const ratio = scores[i] / fullScores[i] + if (ratio >= 0.6) passCount += 1 + if (ratio >= 0.85) excellentCount += 1 + } + + return { + average: round2(average), + median: round2(median), + max, + min, + stdDev: round2(stdDev), + passRate: round2((passCount / countN) * 100), + excellentRate: round2((excellentCount / countN) * 100), + count: countN, + } +} + +/** + * Compute average score from a list of grade record list items. + */ +export function computeAverageScore(scores: number[]): number { + if (scores.length === 0) return 0 + return round2(scores.reduce((a, b) => a + b, 0) / scores.length) +} + +/** + * Build a grade trend point list from raw DB rows (already ordered by date asc). + */ +export function buildGradeTrendPoints( + rows: Array<{ + record: { + createdAt: Date + title: string + score: unknown + fullScore: unknown + type: string + } + }> +): GradeTrendPoint[] { + return rows.map((r) => { + const score = toNumber(r.record.score) + const fullScore = toNumber(r.record.fullScore) + return { + date: r.record.createdAt.toISOString(), + title: r.record.title, + score, + fullScore, + normalizedScore: normalize(score, fullScore), + type: r.record.type as GradeTrendPoint["type"], + } + }) +} + +/** + * Compute the average of normalized scores from a list of trend points. + */ +export function computeTrendAverage(points: GradeTrendPoint[]): number { + if (points.length === 0) return 0 + const avg = + points.reduce((acc, p) => acc + p.normalizedScore, 0) / points.length + return round2(avg) +} + +/** + * Compute class comparison stats for a single class from raw score rows. + */ +export function computeClassComparisonStats( + rows: Array<{ + score: unknown + fullScore: unknown + studentId: string + }> +): Pick< + ClassComparisonItem, + | "averageScore" + | "medianScore" + | "passRate" + | "excellentRate" + | "count" + | "studentCount" +> { + if (rows.length === 0) { + return { + averageScore: 0, + medianScore: 0, + passRate: 0, + excellentRate: 0, + count: 0, + studentCount: 0, + } + } + + const normalized = rows.map((r) => + normalize(toNumber(r.score), toNumber(r.fullScore)) + ) + const sorted = [...normalized].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + const median = + sorted.length % 2 === 0 + ? (sorted[mid - 1] + sorted[mid]) / 2 + : sorted[mid] + const avg = normalized.reduce((a, b) => a + b, 0) / normalized.length + const uniqueStudents = new Set(rows.map((r) => r.studentId)).size + + let passCount = 0 + let excellentCount = 0 + for (const s of normalized) { + if (s >= PASS_THRESHOLD) passCount += 1 + if (s >= EXCELLENT_THRESHOLD) excellentCount += 1 + } + + return { + averageScore: round2(avg), + medianScore: round2(median), + passRate: round2((passCount / normalized.length) * 100), + excellentRate: round2((excellentCount / normalized.length) * 100), + count: normalized.length, + studentCount: uniqueStudents, + } +} + +/** + * Compute subject comparison stats for a single subject from normalized scores. + */ +export function computeSubjectComparisonStats( + scores: number[] +): Pick< + import("./types").SubjectComparisonItem, + "averageScore" | "medianScore" | "passRate" | "excellentRate" | "count" +> { + if (scores.length === 0) { + return { + averageScore: 0, + medianScore: 0, + passRate: 0, + excellentRate: 0, + count: 0, + } + } + + const sorted = [...scores].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + const median = + sorted.length % 2 === 0 + ? (sorted[mid - 1] + sorted[mid]) / 2 + : sorted[mid] + const avg = scores.reduce((a, b) => a + b, 0) / scores.length + + let passCount = 0 + let excellentCount = 0 + for (const s of scores) { + if (s >= PASS_THRESHOLD) passCount += 1 + if (s >= EXCELLENT_THRESHOLD) excellentCount += 1 + } + + return { + averageScore: round2(avg), + medianScore: round2(median), + passRate: round2((passCount / scores.length) * 100), + excellentRate: round2((excellentCount / scores.length) * 100), + count: scores.length, + } +} + +/** + * Default distribution buckets (90-100, 80-89, 70-79, 60-69, <60). + */ +export function createDefaultBuckets(): GradeDistributionBucket[] { + return [ + { label: "90-100", min: 90, max: 100, count: 0 }, + { label: "80-89", min: 80, max: 89, count: 0 }, + { label: "70-79", min: 70, max: 79, count: 0 }, + { label: "60-69", min: 60, max: 69, count: 0 }, + { label: "<60", min: 0, max: 59, count: 0 }, + ] +} + +/** + * Bucketize raw score rows into a grade distribution result. + */ +export function computeGradeDistribution( + rows: RawScoreRow[] +): GradeDistributionResult { + const buckets = createDefaultBuckets() + for (const r of rows) { + const normalized = normalize(toNumber(r.score), toNumber(r.fullScore)) + const rounded = Math.round(normalized) + if (rounded >= 90) buckets[0].count += 1 + else if (rounded >= 80) buckets[1].count += 1 + else if (rounded >= 70) buckets[2].count += 1 + else if (rounded >= 60) buckets[3].count += 1 + else buckets[4].count += 1 + } + return { buckets, totalCount: rows.length } +} + +/** + * Ranking trend entry for a single assessment (grouped by title). + */ +export interface RankingTrendEntry { + date: Date + entries: Array<{ studentId: string; normalized: number }> +} + +/** + * Build ranking trend points from grouped entries. + * Returns points sorted by date ascending. + */ +export function buildRankingTrendPoints( + byTitle: Map, + targetStudentId: string +): RankingTrendPoint[] { + const points: RankingTrendPoint[] = [] + for (const [title, entry] of byTitle.entries()) { + if (entry.entries.length === 0) continue + const sorted = [...entry.entries].sort( + (a, b) => b.normalized - a.normalized + ) + let rank = 0 + let studentEntry: { studentId: string; normalized: number } | null = null + for (let i = 0; i < sorted.length; i += 1) { + const e = sorted[i] + if (e.studentId === targetStudentId) { + rank = i + 1 + studentEntry = e + break + } + } + if (rank <= 0 || !studentEntry) continue + + points.push({ + title, + date: entry.date.toISOString(), + score: studentEntry.normalized, + rank, + totalStudents: sorted.length, + }) + } + + points.sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() + ) + return points +} diff --git a/src/shared/db/schema.ts b/src/shared/db/schema.ts index 8fb33a2..d655149 100644 --- a/src/shared/db/schema.ts +++ b/src/shared/db/schema.ts @@ -1247,7 +1247,7 @@ export const diagnosticReportTypeEnum = mysqlEnum("report_type", ["individual", export const learningDiagnosticReports = mysqlTable("learning_diagnostic_reports", { id: id("id").primaryKey(), - studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }), + studentId: varchar("student_id", { length: 128 }).references(() => users.id, { onDelete: "cascade" }), generatedBy: varchar("generated_by", { length: 128 }).references(() => users.id, { onDelete: "set null" }), reportType: diagnosticReportTypeEnum.default("individual").notNull(), period: varchar("period", { length: 50 }), @@ -1317,3 +1317,23 @@ export const lessonPlanTemplates = mysqlTable("lesson_plan_templates", { }, (table) => ({ typeCreatorIdx: index("lpt_type_creator_idx").on(table.type, table.creatorId), })); + +// --- 25. System Settings (系统设置 - 键值对存储) --- + +export const systemSettings = mysqlTable("system_settings", { + id: id("id").primaryKey(), + /** 设置分组:school_info / security_policy / file_upload / notification_config */ + category: varchar("category", { length: 50 }).notNull(), + /** 设置键名,如 schoolName / passwordMinLength / maxFileSize */ + key: varchar("key", { length: 100 }).notNull(), + /** 设置值(JSON 字符串,支持字符串/数字/布尔/对象) */ + value: text("value").notNull(), + /** 值类型:string / number / boolean / json */ + valueType: varchar("value_type", { length: 20 }).default("string").notNull(), + updatedBy: varchar("updated_by", { length: 128 }), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + categoryKeyIdx: uniqueIndex("ss_category_key_idx").on(table.category, table.key), + categoryIdx: index("ss_category_idx").on(table.category), +}));
成绩记录列表