All files / web/src/db/schema entry-prompts.ts

95.78% Statements 91/95
100% Branches 5/5
60% Functions 3/5
95.78% Lines 91/95

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 962x 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 39x 39x 39x 39x 39x 39x 2x 2x 2x 2x 2x 2x 2x 2x 2x     2x 2x 2x 2x 2x      
import { createId } from '@paralleldrive/cuid2'
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { classrooms } from './classrooms'
import { players } from './players'
import { users } from './users'
 
/**
 * Entry prompt status
 */
export type EntryPromptStatus = 'pending' | 'accepted' | 'declined' | 'expired'
 
/**
 * Entry prompts - teacher requests for parent to have child enter classroom
 *
 * Teachers can prompt parents to have their enrolled child enter the classroom.
 * Prompts have an expiry time and parents can accept or decline.
 *
 * - Accept: Child is entered into classroom (presence record created)
 * - Decline: Teacher is notified, child stays out
 * - Expire: Prompt auto-dismisses (client-side based on expiresAt)
 */
export const entryPrompts = sqliteTable(
  'entry_prompts',
  {
    /** Primary key */
    id: text('id')
      .primaryKey()
      .$defaultFn(() => createId()),
 
    /** Teacher who sent the prompt */
    teacherId: text('teacher_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
 
    /** Student (player) to be prompted to enter */
    playerId: text('player_id')
      .notNull()
      .references(() => players.id, { onDelete: 'cascade' }),
 
    /** Classroom to enter */
    classroomId: text('classroom_id')
      .notNull()
      .references(() => classrooms.id, { onDelete: 'cascade' }),
 
    /** When the prompt expires (ISO timestamp) */
    expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
 
    /** Current status of the prompt */
    status: text('status').notNull().default('pending').$type<EntryPromptStatus>(),
 
    /** Parent who responded (if any) */
    respondedBy: text('responded_by').references(() => users.id),
 
    /** When parent responded */
    respondedAt: integer('responded_at', { mode: 'timestamp' }),
 
    /** When the prompt was created */
    createdAt: integer('created_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
  },
  (table) => ({
    /** Index for finding prompts by teacher */
    teacherIdx: index('idx_entry_prompts_teacher').on(table.teacherId),
 
    /** Index for finding prompts by player */
    playerIdx: index('idx_entry_prompts_player').on(table.playerId),
 
    /** Index for finding prompts by classroom */
    classroomIdx: index('idx_entry_prompts_classroom').on(table.classroomId),
 
    /** Index for filtering by status */
    statusIdx: index('idx_entry_prompts_status').on(table.status),
 
    // Note: Partial unique index (only one pending per player/classroom)
    // is created in migration SQL directly since Drizzle doesn't support WHERE clauses
  })
)
 
export type EntryPrompt = typeof entryPrompts.$inferSelect
export type NewEntryPrompt = typeof entryPrompts.$inferInsert
 
/**
 * Check if a prompt has expired
 */
export function isPromptExpired(prompt: EntryPrompt): boolean {
  return prompt.expiresAt < new Date()
}
 
/**
 * Check if a prompt is still active (pending and not expired)
 */
export function isPromptActive(prompt: EntryPrompt): boolean {
  return prompt.status === 'pending' && !isPromptExpired(prompt)
}