All files / web/src/db/schema skill-tutorial-progress.ts

97% Statements 97/100
100% Branches 2/2
50% Functions 1/2
97% Lines 97/100

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 1012x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 39x 39x 39x 39x 39x 39x 39x 39x 39x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x        
import { createId } from '@paralleldrive/cuid2'
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import { players } from './players'
 
/**
 * Skill Tutorial Progress
 *
 * Tracks whether a student has completed the tutorial for each skill.
 * Tutorial completion is required before a skill can be added to practice rotation
 * (unless teacher override is used).
 */
export const skillTutorialProgress = sqliteTable(
  'skill_tutorial_progress',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => createId()),
 
    /** Foreign key to players table */
    playerId: text('player_id')
      .notNull()
      .references(() => players.id, { onDelete: 'cascade' }),
 
    /**
     * Skill identifier - matches the skill paths from curriculum phases
     * Examples:
     * - "basic.directAddition"
     * - "fiveComplements.4=5-1"
     * - "tenComplements.9=10-1"
     */
    skillId: text('skill_id').notNull(),
 
    // ---- Tutorial Completion ----
 
    /** Whether the student has completed the tutorial for this skill */
    tutorialCompleted: integer('tutorial_completed', { mode: 'boolean' }).notNull().default(false),
 
    /** When the tutorial was completed */
    completedAt: integer('completed_at', { mode: 'timestamp' }),
 
    // ---- Teacher Override ----
 
    /**
     * Teacher can mark a skill as "learned offline" to bypass tutorial requirement.
     * Use case: student learned the technique in person with their teacher.
     */
    teacherOverride: integer('teacher_override', { mode: 'boolean' }).notNull().default(false),
 
    /** When the teacher override was applied */
    overrideAt: integer('override_at', { mode: 'timestamp' }),
 
    /** Optional reason for the override (e.g., "Learned in class with Kehkashan") */
    overrideReason: text('override_reason'),
 
    // ---- Tutorial Skip Tracking ----
 
    /**
     * Number of times the student has skipped the tutorial prompt.
     * Used to surface to teacher if student is repeatedly avoiding tutorials.
     */
    skipCount: integer('skip_count').notNull().default(0),
 
    /** When the tutorial was last skipped */
    lastSkippedAt: integer('last_skipped_at', { mode: 'timestamp' }),
 
    // ---- Metadata ----
 
    createdAt: integer('created_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
 
    updatedAt: integer('updated_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
  },
  (table) => ({
    /** Index for fast lookups by playerId */
    playerIdIdx: index('skill_tutorial_progress_player_id_idx').on(table.playerId),
 
    /** Unique constraint: one record per player per skill */
    playerSkillUnique: uniqueIndex('skill_tutorial_progress_player_skill_unique').on(
      table.playerId,
      table.skillId
    ),
  })
)
 
export type SkillTutorialProgress = typeof skillTutorialProgress.$inferSelect
export type NewSkillTutorialProgress = typeof skillTutorialProgress.$inferInsert
 
/**
 * Check if a skill's tutorial requirement is satisfied.
 * Returns true if:
 * - Tutorial has been completed, OR
 * - Teacher has applied an override
 */
export function isTutorialSatisfied(progress: SkillTutorialProgress | null | undefined): boolean {
  if (!progress) return false
  return progress.tutorialCompleted || progress.teacherOverride
}