// src/pages/premium/OnboardingContainer.js
import React, { useState, useEffect, useRef } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import PremiumWelcome from './PremiumWelcome.js';
import CareerOnboarding from './CareerOnboarding.js';
import FinancialOnboarding from './FinancialOnboarding.js';
import CollegeOnboarding from './CollegeOnboarding.js';
import ReviewPage from './ReviewPage.js';
import { loadDraft, saveDraft, clearDraft } from '../../utils/onboardingDraftApi.js';
import authFetch from '../../utils/authFetch.js';
import { isOnboardingInProgress } from '../../utils/onboardingGuard.js';
const POINTER_KEY = 'premiumOnboardingPointer';
export default function OnboardingContainer() {
const navigate = useNavigate();
const location = useLocation();
const [step, setStep] = useState(0);
const [careerData, setCareerData] = useState({});
const [financialData, setFinancialData] = useState({});
const [collegeData, setCollegeData] = useState({});
const [loaded, setLoaded] = useState(false);
// pointer (safe to store)
const ptrRef = useRef({ id: null, step: 0, skipFin: false, selectedCareer: null });
// ---- 1) one-time load/migrate & hydrate -----------------------
useEffect(() => {
(async () => {
// A) migrate any old local blob (once), then delete it
// B) load pointer
try {
const pointer = JSON.parse(localStorage.getItem(POINTER_KEY) || 'null') || {};
ptrRef.current = {
id: pointer.id || null,
step: Number.isInteger(pointer.step) ? pointer.step : 0,
skipFin: !!pointer.skipFin,
selectedCareer: pointer.selectedCareer || JSON.parse(localStorage.getItem('selectedCareer') || 'null')
};
} catch { /* ignore */ }
// C) fetch draft from server (source of truth)
const draft = await loadDraft();
if (draft) {
ptrRef.current.id = draft.id; // ensure we have it
setStep(draft.step ?? ptrRef.current.step ?? 0);
const d = draft.data || {};
setCareerData(d.careerData || {});
setFinancialData(d.financialData || {});
setCollegeData(d.collegeData || {});
// 🔒 Prime autosave baselines so we DON'T post empty slices
try {
prevCareerJsonRef.current = JSON.stringify(d.careerData || {});
prevFinancialJsonRef.current = JSON.stringify(d.financialData || {});
prevCollegeJsonRef.current = JSON.stringify(d.collegeData || {});
} catch {}
} else {
// no server draft yet: seed with minimal data from pointer/local selectedCareer
setStep(ptrRef.current.step || 0);
if (ptrRef.current.selectedCareer?.title) {
setCareerData(cd => ({
...cd,
career_name: ptrRef.current.selectedCareer.title,
soc_code: ptrRef.current.selectedCareer.soc_code || ''
}));
}
}
// D) pick up any navigation state (e.g., selectedSchool)
const navSchool =
location.state?.selectedSchool ??
location.state?.premiumOnboardingState?.selectedSchool;
if (navSchool) {
setCollegeData(cd => ({
...cd,
selected_school : typeof navSchool === 'string' ? navSchool : (navSchool.INSTNM || ''),
selected_program: typeof navSchool === 'object' ? (navSchool.CIPDESC || cd.selected_program || '') : cd.selected_program,
program_type : typeof navSchool === 'object' ? (navSchool.CREDDESC || cd.program_type || '') : cd.program_type,
}));
// keep baseline in sync so autosave doesn't blast empty collegeData
try {
const merged = {
...(draft?.data?.collegeData || {}),
selected_school : typeof navSchool === 'string' ? navSchool : (navSchool.INSTNM || ''),
selected_program: typeof navSchool === 'object' ? (navSchool.CIPDESC || '') : '',
program_type : typeof navSchool === 'object' ? (navSchool.CREDDESC || '') : '',
};
prevCollegeJsonRef.current = JSON.stringify(merged);
} catch {}
}
setLoaded(true);
})();
}, [location.state]);
// ---- 2) debounced autosave — send only changed slices ----------
const prevCareerJsonRef = useRef('');
const prevFinancialJsonRef = useRef('');
const prevCollegeJsonRef = useRef('');
useEffect(() => {
if (!loaded) return;
const t = setTimeout(async () => {
const cj = JSON.stringify(careerData);
const fj = JSON.stringify(financialData);
const col = JSON.stringify(collegeData);
const nonEmpty = (s) => s && s !== '{}' && s !== 'null';
const changedCareer = cj !== prevCareerJsonRef.current && nonEmpty(cj);
const changedFinancial = fj !== prevFinancialJsonRef.current && nonEmpty(fj);
const changedCollege = col !== prevCollegeJsonRef.current && nonEmpty(col);
const somethingChanged = changedCareer || changedFinancial || changedCollege;
// Always update the local pointer, but DO NOT POST an empty data object.
const pointer = {
id: ptrRef.current.id,
step,
skipFin: !!careerData.skipFinancialStep,
selectedCareer: (careerData.career_name || careerData.soc_code)
? { title: careerData.career_name, soc_code: careerData.soc_code }
: JSON.parse(localStorage.getItem('selectedCareer') || 'null'),
};
ptrRef.current = pointer;
localStorage.setItem(POINTER_KEY, JSON.stringify(pointer));
if (!somethingChanged) return; // ← prevent the `{ data:{} }` POST
// Build a payload that includes only changed slices
const payload = { id: ptrRef.current.id, step, data: {} };
if (changedCareer) payload.data.careerData = careerData;
if (changedFinancial) payload.data.financialData = financialData;
if (changedCollege) payload.data.collegeData = collegeData;
const resp = await saveDraft(payload);
// update baselines only for the slices we actually sent
if (changedCareer) prevCareerJsonRef.current = cj;
if (changedFinancial) prevFinancialJsonRef.current = fj;
if (changedCollege) prevCollegeJsonRef.current = col;
// keep pointer id in sync with server
ptrRef.current.id = resp.id;
}, 400);
return () => clearTimeout(t);
}, [loaded, step, careerData, financialData, collegeData]);
// ---- nav helpers ------------------------------------------------
const nextStep = () => setStep((s) => s + 1);
const prevStep = () => setStep((s) => Math.max(0, s - 1));
// Steps: Welcome, Career, (Financial?), College, Review => 4 or 5 total
const finishImmediately = () => setStep(skipFin ? 3 : 4);
// ---- final submit (unchanged + cleanup) -------------------------
async function handleFinalSubmit() {
try {
// 1) scenario upsert
const scenarioPayload = { ...careerData, id: careerData.career_profile_id || undefined };
const scenarioRes = await authFetch('/api/premium/career-profile', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(scenarioPayload)
});
if (!scenarioRes || !scenarioRes.ok) throw new Error('Failed to save (or update) career profile');
const { career_profile_id: finalId } = await scenarioRes.json();
if (!finalId) throw new Error('No career_profile_id returned by server');
// 2) financial profile
const finRes = await authFetch('/api/premium/financial-profile', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(financialData)
});
if (!finRes || !finRes.ok) throw new Error('Failed to save financial profile');
// 3) college profile (conditional)
if (['currently_enrolled','prospective_student'].includes(careerData.college_enrollment_status)) {
const merged = {
...collegeData,
career_profile_id: finalId,
college_enrollment_status: careerData.college_enrollment_status,
is_in_state: !!collegeData.is_in_state,
is_in_district: !!collegeData.is_in_district,
is_online: !!collegeData.is_online,
loan_deferral_until_graduation: !!collegeData.loan_deferral_until_graduation
};
// numeric normalization (your existing parse rules apply)
const nums = ['existing_college_debt','extra_payment','tuition','tuition_paid','interest_rate',
'loan_term','credit_hours_per_year','credit_hours_required','hours_completed',
'program_length','expected_salary','annual_financial_aid'];
nums.forEach(k => { const n = Number(merged[k]); merged[k] = Number.isFinite(n) ? n : 0; });
const colRes = await authFetch('/api/premium/college-profile', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(merged)
});
if (!colRes || !colRes.ok) throw new Error('Failed to save college profile');
}
// 4) UX handoff + cleanup
const picked = { code: careerData.soc_code, title: careerData.career_name };
sessionStorage.setItem('skipMissingModalFor', String(finalId));
localStorage.setItem('selectedCareer', JSON.stringify(picked));
localStorage.removeItem('lastSelectedCareerProfileId');
// 🔐 cleanup: remove server draft + pointer
await clearDraft();
localStorage.removeItem(POINTER_KEY);
sessionStorage.setItem('suppressOnboardingGuard', '1');
navigate(`/career-roadmap/${finalId}`, { state: { fromOnboarding: true, selectedCareer: picked } });
} catch (err) {
console.error('Error in final submit =>', err);
alert(err.message || 'Failed to finalize onboarding.');
}
}
const skipFin = !!careerData.skipFinancialStep;
const steps = [