import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api, hasSessionToken } from '@/services/backend'; import { getLatestByPhase } from '@/services/assessmentService'; import { getProgramProgress } from '@/services/programService'; import { PROGRAM_SESSION_COUNT } from '@/data/program/sessions'; /** Sessions 1..(N-1) done ⇒ learner is on final session; post-test may open manually. */ const POST_TEST_UNLOCK_MIN_COMPLETED_SESSIONS = PROGRAM_SESSION_COUNT - 1; export type LearningStep = | 'registered' | 'pre_test_done' | 'training_in_progress' | 'training_completed' | 'post_test_done' | 'completed'; export interface LearningProgress { user_id: string; current_step: LearningStep; pre_test_completed_at: string | null; training_started_at: string | null; training_completed_at: string | null; post_test_completed_at: string | null; updated_at?: string; } const getStorageKey = (userId: string) => `learning-progress:${userId}`; const defaultProgress = (userId: string): LearningProgress => ({ user_id: userId, current_step: 'registered', pre_test_completed_at: null, training_started_at: null, training_completed_at: null, post_test_completed_at: null }); const readLocalProgress = (userId: string): LearningProgress => { try { const saved = localStorage.getItem(getStorageKey(userId)); return saved ? (JSON.parse(saved) as LearningProgress) : defaultProgress(userId); } catch { return defaultProgress(userId); } }; const writeLocalProgress = (progress: LearningProgress) => { localStorage.setItem(getStorageKey(progress.user_id), JSON.stringify(progress)); }; /** Mirrors backend deriveCurrentStep (timestamps are authoritative). */ const deriveStep = (progress: LearningProgress): LearningStep => { if (progress.post_test_completed_at) return 'completed'; if (progress.training_completed_at) return 'training_completed'; if (progress.training_started_at && progress.pre_test_completed_at) return 'training_in_progress'; if (progress.pre_test_completed_at) return 'pre_test_done'; return 'registered'; }; export function useLearningJourney(userId?: string) { /** Offline guest ids are `guest-…` and have no backend session. */ const useLocalOnly = !hasSessionToken() || Boolean(userId?.startsWith('guest-')); const [progress, setProgress] = useState(null); const [backendPermissions, setBackendPermissions] = useState<{ canTakePreTest: boolean; canOpenTraining: boolean; canTakePostTest: boolean; isCompleted: boolean; } | null>(null); const [loading, setLoading] = useState(true); /** Skeleton / blocking load only on first fetch — silent on reloadProgress. */ const hasLoadedOnceRef = useRef(false); const mapBackendJourney = useCallback((journey: { currentStep: LearningStep; preTestCompletedAt: string | null; trainingStartedAt: string | null; trainingCompletedAt: string | null; postTestCompletedAt: string | null; lastActivityAt: string | null; }): LearningProgress => ({ user_id: userId ?? '', current_step: journey.currentStep, pre_test_completed_at: journey.preTestCompletedAt, training_started_at: journey.trainingStartedAt, training_completed_at: journey.trainingCompletedAt, post_test_completed_at: journey.postTestCompletedAt, updated_at: journey.lastActivityAt ?? undefined, }), [userId]); const mergeAssessmentCompletion = useCallback( async (base: LearningProgress): Promise => { try { const [preAttempt, postAttempt] = await Promise.all([ getLatestByPhase(base.user_id, 'pre'), getLatestByPhase(base.user_id, 'post'), ]); const merged: LearningProgress = { ...base, pre_test_completed_at: base.pre_test_completed_at ?? preAttempt?.completedAt ?? null, training_started_at: base.training_started_at ?? base.pre_test_completed_at ?? preAttempt?.completedAt ?? null, post_test_completed_at: base.post_test_completed_at ?? postAttempt?.completedAt ?? null, }; merged.current_step = deriveStep(merged); return merged; } catch { return base; } }, [] ); const loadProgress = useCallback(async () => { if (!userId) { setProgress(null); setBackendPermissions(null); setLoading(false); return; } const isInitialLoad = !hasLoadedOnceRef.current; if (isInitialLoad) { setLoading(true); } if (useLocalOnly) { const local = await mergeAssessmentCompletion(readLocalProgress(userId)); setProgress(local); setBackendPermissions(null); hasLoadedOnceRef.current = true; setLoading(false); return; } try { const journey = await api.get<{ currentStep: LearningStep; preTestCompletedAt: string | null; trainingStartedAt: string | null; trainingCompletedAt: string | null; postTestCompletedAt: string | null; lastActivityAt: string | null; permissions: { canTakePreTest: boolean; canOpenTraining: boolean; canTakePostTest: boolean; isCompleted: boolean; }; }>('/journey'); const mapped = mapBackendJourney(journey); const merged = await mergeAssessmentCompletion(mapped); setProgress(merged); setBackendPermissions(journey.permissions); } catch (error) { console.error('Error loading learning progress:', error); const local = readLocalProgress(userId); const merged = await mergeAssessmentCompletion(local); setProgress(merged); setBackendPermissions(null); } finally { hasLoadedOnceRef.current = true; setLoading(false); } }, [mapBackendJourney, mergeAssessmentCompletion, userId, useLocalOnly]); useEffect(() => { hasLoadedOnceRef.current = false; }, [userId]); useEffect(() => { loadProgress(); }, [loadProgress]); const saveProgress = useCallback( async (next: LearningProgress) => { // Optimistic update - فوری state را update کن setProgress(next); if (useLocalOnly) { writeLocalProgress(next); return; } // Backend is source of truth for authenticated users. // Do not write journey state directly from frontend. writeLocalProgress(next); }, [useLocalOnly] ); const markPreTestDone = useCallback(async (completedAt?: string) => { const now = completedAt ?? new Date().toISOString(); const base = progress ?? { user_id: userId ?? '', current_step: 'registered' as const, pre_test_completed_at: null, training_started_at: null, training_completed_at: null, post_test_completed_at: null }; const next = { ...base, current_step: 'training_in_progress' as const, pre_test_completed_at: base.pre_test_completed_at || now, training_started_at: base.training_started_at || now }; await saveProgress(next); return true; }, [progress, saveProgress, userId]); const markTrainingDone = useCallback(async () => { if (!userId) return false; if (!useLocalOnly) { await loadProgress(); return true; } const now = new Date().toISOString(); const base = progress ?? defaultProgress(userId); if (base.training_completed_at) { return true; } const next = { ...base, user_id: userId, current_step: 'training_completed' as const, training_started_at: base.training_started_at || now, training_completed_at: now }; await saveProgress(next); return true; }, [loadProgress, progress, saveProgress, useLocalOnly, userId]); const markPostTestDone = useCallback(async (completedAt?: string) => { if (!userId) return false; const now = completedAt ?? new Date().toISOString(); const base = progress ?? defaultProgress(userId); if (base.post_test_completed_at) { return true; } const next = { ...base, user_id: userId, current_step: 'completed' as const, post_test_completed_at: now }; await saveProgress(next); return true; }, [progress, saveProgress, userId]); const derivedPermissions = useMemo(() => { const completedSessions = userId ? getProgramProgress(userId).completedSessions.length : 0; const reachedFinalSession = Boolean(progress?.training_completed_at) || completedSessions >= POST_TEST_UNLOCK_MIN_COMPLETED_SESSIONS; return { canTakePreTest: !progress?.pre_test_completed_at, canOpenTraining: Boolean(progress?.pre_test_completed_at), canTakePostTest: Boolean( progress?.pre_test_completed_at && reachedFinalSession && !progress?.post_test_completed_at ), isCompleted: Boolean(progress?.post_test_completed_at) }; }, [progress, userId]); const permissions = backendPermissions ?? derivedPermissions; return { progress, loading, permissions, reloadProgress: loadProgress, markPreTestDone, markTrainingDone, markPostTestDone }; }