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 101 102 103 104 105 106 107 108 109 110 111 | 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 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 type {
LinearCategoryStatus,
LinearReadinessFrontier,
LinearReadinessSkillState,
LinearReadinessState,
LinearReadyCategory,
} from '@/lib/curriculum/linear-readiness-service'
import { api } from '@/lib/queryClient'
import { curriculumKeys, sessionPlanKeys } from '@/lib/queryKeys'
export type {
LinearCategoryStatus,
LinearReadinessFrontier,
LinearReadinessSkillState,
LinearReadinessState,
LinearReadyCategory,
}
/**
* True when the readiness flag is on and no category currently feeds number
* sentences. Client-safe (the service module is server-only).
*/
export function isLinearLocked(state: LinearReadinessState | undefined | null): boolean {
if (!state?.enabled) return false
return !state.categories.some((c) => c.status === 'ready')
}
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}`)
}
const state = (await response.json()) as LinearReadinessState
// Old replicas (rolling deploy) may still send categories without `status`.
return {
...state,
categories: state.categories.map((c) => ({
...c,
status: c.status ?? (c.vetoed ? 'vetoed' : 'ready'),
})),
}
}
/** 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 }
}
|