All files / web/src/hooks useLinearReadiness.ts

30.95% Statements 26/84
100% Branches 0/0
0% Functions 0/3
30.95% Lines 26/84

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 851x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x               1x 1x 1x             1x 1x 1x                                                                                            
/**
 * Hook for linear-readiness (L3): which skill categories have "aged out" onto
 * number sentences for a student, and the per-category teacher veto.
 *
 * Reads/writes `/api/curriculum/[playerId]/linear-veto`. All server state via React
 * Query (no `fetch()` in components, no `useState` for server data).
 */
 
'use client'
 
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { api } from '@/lib/queryClient'
import { curriculumKeys, sessionPlanKeys } from '@/lib/queryKeys'
import type {
  LinearReadinessState,
  LinearReadyCategory,
} from '@/lib/curriculum/linear-readiness-service'
 
export type { LinearReadinessState, LinearReadyCategory }
 
async function fetchLinearReadiness(playerId: string): Promise<LinearReadinessState> {
  const response = await api(`curriculum/${playerId}/linear-veto`)
  if (!response.ok) {
    throw new Error(`Failed to load linear readiness: ${response.statusText}`)
  }
  return response.json()
}
 
/** Fetch the derived linear-ready categories + veto state for a student. */
export function useLinearReadiness(playerId: string | null) {
  return useQuery({
    queryKey: curriculumKeys.linearReadiness(playerId ?? ''),
    queryFn: () => fetchLinearReadiness(playerId!),
    enabled: !!playerId,
  })
}
 
/** Mutations to veto / un-veto a skill category off number sentences. */
export function useLinearReadinessVeto(playerId: string | null) {
  const queryClient = useQueryClient()

  const invalidate = () => {
    if (!playerId) return
    queryClient.invalidateQueries({ queryKey: curriculumKeys.linearReadiness(playerId) })
    // Re-planning depends on vetoes; refresh any session-plan views too.
    queryClient.invalidateQueries({ queryKey: sessionPlanKeys.list(playerId) })
  }

  const setVeto = useMutation({
    mutationFn: async ({ category, reason }: { category: string; reason?: string }) => {
      if (!playerId) throw new Error('No player selected')
      const response = await api(`curriculum/${playerId}/linear-veto`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ category, reason }),
      })
      if (!response.ok) {
        const error = await response.json().catch(() => ({}))
        throw new Error(error.error || 'Failed to veto category')
      }
      return response.json()
    },
    onSuccess: invalidate,
  })

  const clearVeto = useMutation({
    mutationFn: async ({ category }: { category: string }) => {
      if (!playerId) throw new Error('No player selected')
      const response = await api(`curriculum/${playerId}/linear-veto`, {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ category }),
      })
      if (!response.ok) {
        const error = await response.json().catch(() => ({}))
        throw new Error(error.error || 'Failed to lift veto')
      }
      return response.json()
    },
    onSuccess: invalidate,
  })

  return { setVeto, clearVeto }
}