Skip to content

Commit 949474e

Browse files
committed
fix: replace useEffect+setState with useReducer in CqiWeightsPanel
1 parent 99fb50d commit 949474e

File tree

1 file changed

+60
-32
lines changed

1 file changed

+60
-32
lines changed

src/main/webapp/src/components/CqiWeightsPanel.tsx

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react';
1+
import { useReducer } from 'react';
22
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
33
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
44
import { 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+
2465
export 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

Comments
 (0)