forked from Lab-Lab-Lab/CPR-Music
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseActivityProgress.js
More file actions
225 lines (188 loc) Β· 7.48 KB
/
useActivityProgress.js
File metadata and controls
225 lines (188 loc) Β· 7.48 KB
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/**
* useActivityProgress Hook
*
* Manages activity progress state for DAW study protocol.
* Integrates with DAWActivityLogger and backend API.
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { getDAWActivityLogger } from '../lib/activity/DAWActivityLogger';
import {
getActivityProgress,
mutateLogActivityEvent,
mutateSubmitActivityStep,
mutateSaveQuestionResponse,
} from '../api';
import {
isStepComplete,
getStepProgress,
getMissingOperations,
ACTIVITY_REQUIREMENTS,
} from '../lib/activity/activityConstants';
export function useActivityProgress({ slug, assignmentId, initialStep = 1, email = null }) {
// Progress state
const [currentStep, setCurrentStep] = useState(initialStep);
const [stepCompletions, setStepCompletions] = useState({});
const [activityLogs, setActivityLogs] = useState([]);
const [questionResponses, setQuestionResponses] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
// Track if we've loaded initial progress
const hasLoadedProgress = useRef(false);
// Store email in ref to avoid re-renders
const emailRef = useRef(email);
// Get logger instance
const logger = getDAWActivityLogger();
// Sync currentStep with initialStep ONLY on initial mount (before progress loads)
// This fixes NaN issue on first render without overwriting updates from submitStep
useEffect(() => {
if (!hasLoadedProgress.current && !isNaN(initialStep)) {
console.log('π Initial sync of currentStep with initialStep:', initialStep);
setCurrentStep(initialStep);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialStep]); // Only depend on initialStep, not currentStep
// Load progress from backend on mount
useEffect(() => {
if (!slug || !assignmentId || hasLoadedProgress.current) return;
const loadProgress = async () => {
try {
console.log('π₯ Loading progress for assignment:', assignmentId);
const progress = await getActivityProgress({ slug, assignmentId });
console.log('π₯ Loaded progress from backend:', progress);
if (progress) {
console.log('π₯ Setting state from progress:', {
current_step: progress.current_step,
step_completions: progress.step_completions,
activity_logs_count: progress.activity_logs?.length || 0,
});
setCurrentStep(progress.current_step);
setStepCompletions(progress.step_completions || {});
setActivityLogs(progress.activity_logs || []);
setQuestionResponses(progress.question_responses || {});
} else {
console.log('π₯ No progress found, starting fresh');
}
hasLoadedProgress.current = true;
setIsLoading(false);
} catch (error) {
console.error('Failed to load activity progress:', error);
setIsLoading(false);
}
};
loadProgress();
}, [slug, assignmentId]);
// Log operation and update local state
const logOperation = useCallback(async (operation, data = {}) => {
console.log('π useActivityProgress.logOperation called:', { operation, data, slug, assignmentId, currentStep });
if (!slug || !assignmentId) {
console.warn('β οΈ Cannot log operation - missing slug or assignmentId:', { slug, assignmentId });
return;
}
try {
// Log to backend
console.log('π‘ Sending operation to backend:', operation);
const logEvent = mutateLogActivityEvent({ slug, assignmentId });
const updatedProgress = await logEvent({
operation,
step: currentStep,
data,
email: emailRef.current, // Include email from Qualtrics
});
console.log('β
Backend responded with updated progress:', updatedProgress?.step_completions);
// Also log to DAW logger for local analytics
logger.logEvent(operation, data);
// Update local state with server response
if (updatedProgress?.step_completions) {
setStepCompletions(updatedProgress.step_completions);
setActivityLogs(updatedProgress.activity_logs || []);
}
return updatedProgress;
} catch (error) {
console.error('β Failed to log operation:', error);
throw error;
}
}, [slug, assignmentId, currentStep, logger]);
// Save question response
const saveResponse = useCallback(async (questionId, response) => {
if (!slug || !assignmentId) return;
// Update local state optimistically before the API call
setQuestionResponses(prev => ({
...prev,
[questionId]: response,
}));
try {
const saveResponseFn = mutateSaveQuestionResponse({ slug, assignmentId });
const updatedProgress = await saveResponseFn({ questionId, response });
return updatedProgress;
} catch (error) {
console.error('Failed to save response:', error);
throw error;
}
}, [slug, assignmentId]);
// Submit current step and advance
const submitStep = useCallback(async (responses = {}, stepToSubmit = null) => {
if (!slug || !assignmentId) {
console.error('Cannot submit: missing slug or assignmentId', { slug, assignmentId });
throw new Error('Assignment not loaded yet. Please wait and try again.');
}
// Use provided step or fall back to currentStep from state
const step = stepToSubmit ?? currentStep;
console.log('π€ Submitting step:', step);
setIsSubmitting(true);
try {
const submitFn = mutateSubmitActivityStep({ slug, assignmentId });
const updatedProgress = await submitFn({
questionResponses: {
...questionResponses,
...responses,
},
step,
});
console.log('π₯ Backend returned current_step:', updatedProgress.current_step);
// Update local state
setCurrentStep(updatedProgress.current_step);
setStepCompletions(updatedProgress.step_completions);
setActivityLogs(updatedProgress.activity_logs || []);
setQuestionResponses(updatedProgress.question_responses);
setIsSubmitting(false);
return updatedProgress;
} catch (error) {
console.error('Failed to submit step:', error);
setIsSubmitting(false);
throw error;
}
}, [slug, assignmentId, questionResponses, currentStep]);
// Check if the VIEWED step (initialStep from URL) can be submitted
// Use initialStep, not currentStep, because currentStep is backend state which may be stale
const canSubmit = useCallback(() => {
return isStepComplete(initialStep, stepCompletions, activityLogs);
}, [initialStep, stepCompletions, activityLogs]);
// Get progress percentage for the VIEWED step
const progress = useCallback(() => {
return getStepProgress(initialStep, stepCompletions, activityLogs);
}, [initialStep, stepCompletions, activityLogs]);
// Get missing operations for the VIEWED step
const missingOperations = useCallback(() => {
return getMissingOperations(initialStep, stepCompletions, activityLogs);
}, [initialStep, stepCompletions, activityLogs]);
// Get step requirements for the VIEWED step
const requirements = ACTIVITY_REQUIREMENTS[initialStep] || { required: [] };
return {
// State
currentStep,
stepCompletions,
questionResponses,
isLoading,
isSubmitting,
// Computed
canSubmit: canSubmit(),
progress: progress(),
missingOperations: missingOperations(),
requirements,
// Methods
logOperation,
saveResponse,
submitStep,
};
}
export default useActivityProgress;