1- import { useState , useEffect } from 'react' ;
1+ import { useReducer } from 'react' ;
22import { useMutation , useQuery , useQueryClient } from '@tanstack/react-query' ;
33import { Card , CardContent , CardHeader , CardTitle } from '@/components/ui/card' ;
44import { Button } from '@/components/ui/button' ;
@@ -21,8 +21,50 @@ interface CqiWeightsPanelProps {
2121 disabled ?: boolean ;
2222}
2323
24+ interface WeightState {
25+ effort : number ;
26+ loc : number ;
27+ temporal : number ;
28+ ownership : number ;
29+ }
30+
31+ type WeightAction =
32+ | { type : 'SET_EFFORT' ; value : number }
33+ | { type : 'SET_LOC' ; value : number }
34+ | { type : 'SET_TEMPORAL' ; value : number }
35+ | { type : 'SET_OWNERSHIP' ; value : number }
36+ | { type : 'RESET' ; state : WeightState } ;
37+
38+ function weightReducer ( state : WeightState , action : WeightAction ) : WeightState {
39+ const clamp = ( v : number ) => Math . max ( 0 , Math . min ( 100 , v ) ) ;
40+ switch ( action . type ) {
41+ case 'SET_EFFORT' :
42+ return { ...state , effort : clamp ( action . value ) } ;
43+ case 'SET_LOC' :
44+ return { ...state , loc : clamp ( action . value ) } ;
45+ case 'SET_TEMPORAL' :
46+ return { ...state , temporal : clamp ( action . value ) } ;
47+ case 'SET_OWNERSHIP' :
48+ return { ...state , ownership : clamp ( action . value ) } ;
49+ case 'RESET' :
50+ return action . state ;
51+ }
52+ }
53+
54+ function toPercentState ( data : CqiWeightsData ) : WeightState {
55+ return {
56+ effort : Math . round ( data . effortBalance * 100 ) ,
57+ loc : Math . round ( data . locBalance * 100 ) ,
58+ temporal : Math . round ( data . temporalSpread * 100 ) ,
59+ ownership : Math . round ( data . ownershipSpread * 100 ) ,
60+ } ;
61+ }
62+
63+ const DEFAULT_STATE : WeightState = { effort : 55 , loc : 25 , temporal : 5 , ownership : 15 } ;
64+
2465export default function CqiWeightsPanel ( { exerciseId, disabled } : CqiWeightsPanelProps ) {
2566 const queryClient = useQueryClient ( ) ;
67+ const [ state , dispatch ] = useReducer ( weightReducer , DEFAULT_STATE ) ;
2668
2769 const { data : weights , isLoading } = useQuery < CqiWeightsData > ( {
2870 queryKey : [ 'cqiWeights' , exerciseId ] ,
@@ -31,27 +73,15 @@ export default function CqiWeightsPanel({ exerciseId, disabled }: CqiWeightsPane
3173 credentials : 'include' ,
3274 } ) ;
3375 if ( ! response . ok ) throw new Error ( 'Failed to fetch CQI weights' ) ;
34- return response . json ( ) ;
76+ const data : CqiWeightsData = await response . json ( ) ;
77+ dispatch ( { type : 'RESET' , state : toPercentState ( data ) } ) ;
78+ return data ;
3579 } ,
3680 enabled : ! ! exerciseId ,
3781 } ) ;
3882
39- const [ effort , setEffort ] = useState ( 55 ) ;
40- const [ loc , setLoc ] = useState ( 25 ) ;
41- const [ temporal , setTemporal ] = useState ( 5 ) ;
42- const [ ownership , setOwnership ] = useState ( 15 ) ;
43-
44- useEffect ( ( ) => {
45- if ( weights ) {
46- setEffort ( Math . round ( weights . effortBalance * 100 ) ) ;
47- setLoc ( Math . round ( weights . locBalance * 100 ) ) ;
48- setTemporal ( Math . round ( weights . temporalSpread * 100 ) ) ;
49- setOwnership ( Math . round ( weights . ownershipSpread * 100 ) ) ;
50- }
51- } , [ weights ] ) ;
52-
53- const total = effort + loc + temporal + ownership ;
54- const isValid = total === 100 && effort >= 0 && loc >= 0 && temporal >= 0 && ownership >= 0 ;
83+ const total = state . effort + state . loc + state . temporal + state . ownership ;
84+ const isValid = total === 100 && state . effort >= 0 && state . loc >= 0 && state . temporal >= 0 && state . ownership >= 0 ;
5585
5686 const saveMutation = useMutation ( {
5787 mutationFn : async ( ) => {
@@ -60,10 +90,10 @@ export default function CqiWeightsPanel({ exerciseId, disabled }: CqiWeightsPane
6090 credentials : 'include' ,
6191 headers : { 'Content-Type' : 'application/json' } ,
6292 body : JSON . stringify ( {
63- effortBalance : effort / 100 ,
64- locBalance : loc / 100 ,
65- temporalSpread : temporal / 100 ,
66- ownershipSpread : ownership / 100 ,
93+ effortBalance : state . effort / 100 ,
94+ locBalance : state . loc / 100 ,
95+ temporalSpread : state . temporal / 100 ,
96+ ownershipSpread : state . ownership / 100 ,
6797 } ) ,
6898 } ) ;
6999 if ( ! response . ok ) throw new Error ( 'Failed to save weights' ) ;
@@ -99,11 +129,11 @@ export default function CqiWeightsPanel({ exerciseId, disabled }: CqiWeightsPane
99129 if ( isLoading ) return null ;
100130
101131 const fields = [
102- { label : 'Effort Balance' , value : effort , setter : setEffort } ,
103- { label : 'LoC Balance' , value : loc , setter : setLoc } ,
104- { label : 'Temporal Spread' , value : temporal , setter : setTemporal } ,
105- { label : 'Ownership Spread' , value : ownership , setter : setOwnership } ,
106- ] as const ;
132+ { label : 'Effort Balance' , value : state . effort , action : 'SET_EFFORT' as const } ,
133+ { label : 'LoC Balance' , value : state . loc , action : 'SET_LOC' as const } ,
134+ { label : 'Temporal Spread' , value : state . temporal , action : 'SET_TEMPORAL' as const } ,
135+ { label : 'Ownership Spread' , value : state . ownership , action : 'SET_OWNERSHIP' as const } ,
136+ ] ;
107137
108138 return (
109139 < Card >
@@ -113,14 +143,12 @@ export default function CqiWeightsPanel({ exerciseId, disabled }: CqiWeightsPane
113143 < Settings className = "h-4 w-4" />
114144 CQI Weights
115145 </ CardTitle >
116- < Badge variant = { weights ?. isDefault ? 'secondary' : 'default' } >
117- { weights ?. isDefault ? 'Default' : 'Custom' }
118- </ Badge >
146+ < Badge variant = { weights ?. isDefault ? 'secondary' : 'default' } > { weights ?. isDefault ? 'Default' : 'Custom' } </ Badge >
119147 </ div >
120148 </ CardHeader >
121149 < CardContent className = "space-y-3" >
122150 < div className = "grid grid-cols-2 gap-3" >
123- { fields . map ( ( { label, value, setter } ) => (
151+ { fields . map ( ( { label, value, action } ) => (
124152 < div key = { label } className = "space-y-1" >
125153 < Label className = "text-xs" > { label } </ Label >
126154 < div className = "flex items-center gap-1" >
@@ -129,7 +157,7 @@ export default function CqiWeightsPanel({ exerciseId, disabled }: CqiWeightsPane
129157 min = { 0 }
130158 max = { 100 }
131159 value = { value }
132- onChange = { ( e ) => setter ( Math . max ( 0 , Math . min ( 100 , parseInt ( e . target . value ) || 0 ) ) ) }
160+ onChange = { e => dispatch ( { type : action , value : parseInt ( e . target . value ) || 0 } ) }
133161 disabled = { disabled }
134162 className = "h-8 text-sm"
135163 />
0 commit comments