Pulled in selected values for premium onboarding, added Expected Graduation Date calculation
This commit is contained in:
parent
f00bc75bb9
commit
b96da9f7dd
@ -1,10 +0,0 @@
|
|||||||
ARG APPPORT=5000
|
|
||||||
FROM node:20-slim
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN apt-get update -y && apt-get install -y --no-install-recommends build-essential python3 make g++ && rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN npm ci --omit=dev --ignore-scripts
|
|
||||||
COPY . .
|
|
||||||
ENV PORT=5000
|
|
||||||
EXPOSE 5000
|
|
||||||
CMD ["node","backend/server.js"]
|
|
16
Dockerfile.server1
Normal file
16
Dockerfile.server1
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM node:20-bullseye AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# ---- native build deps ----
|
||||||
|
RUN apt-get update -y && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
build-essential python3 pkg-config && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY public/ /app/public/
|
||||||
|
RUN npm ci --unsafe-perm
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["node", "backend/server1.js"]
|
@ -579,9 +579,11 @@ app.get('/api/areas', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use env when present (Docker), fall back for local dev
|
// Use env when present (Docker), fall back for local dev
|
||||||
const salaryDbPath =
|
const salaryDbPath =
|
||||||
process.env.SALARY_DB || '/app/data/salary_info.db';
|
process.env.SALARY_DB_PATH // ← preferred
|
||||||
|
|| process.env.SALARY_DB // ← legacy
|
||||||
|
|| '/app/salary_info.db'; // final fallback
|
||||||
|
|
||||||
const salaryDb = new sqlite3.Database(
|
const salaryDb = new sqlite3.Database(
|
||||||
salaryDbPath,
|
salaryDbPath,
|
||||||
sqlite3.OPEN_READONLY,
|
sqlite3.OPEN_READONLY,
|
@ -2452,18 +2452,30 @@ app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, asyn
|
|||||||
FINANCIAL PROFILES
|
FINANCIAL PROFILES
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
// GET /api/premium/financial-profile
|
||||||
try {
|
app.get('/api/premium/financial-profile', auth, (req, res) => {
|
||||||
const [rows] = await pool.query(`
|
const uid = req.userId;
|
||||||
SELECT *
|
db.query('SELECT * FROM financial_profile WHERE user_id=?', [uid],
|
||||||
FROM financial_profiles
|
(err, rows) => {
|
||||||
WHERE user_id = ?
|
if (err) return res.status(500).json({ error:'DB error' });
|
||||||
`, [req.id]);
|
|
||||||
res.json(rows[0] || {});
|
if (!rows.length) {
|
||||||
} catch (error) {
|
// ←———— send a benign default instead of 404
|
||||||
console.error('Error fetching financial profile:', error);
|
return res.json({
|
||||||
res.status(500).json({ error: 'Failed to fetch financial profile' });
|
current_salary: 0,
|
||||||
}
|
additional_income: 0,
|
||||||
|
monthly_expenses: 0,
|
||||||
|
monthly_debt_payments: 0,
|
||||||
|
retirement_savings: 0,
|
||||||
|
emergency_fund: 0,
|
||||||
|
retirement_contribution: 0,
|
||||||
|
emergency_contribution: 0,
|
||||||
|
extra_cash_emergency_pct: 50,
|
||||||
|
extra_cash_retirement_pct: 50
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.json(rows[0]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
||||||
|
@ -8,6 +8,11 @@ services:
|
|||||||
<<: *with-env
|
<<: *with-env
|
||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG}
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG}
|
||||||
expose: ["${SERVER1_PORT}"]
|
expose: ["${SERVER1_PORT}"]
|
||||||
|
environment:
|
||||||
|
SALARY_DB_PATH: /app/salary_info.db
|
||||||
|
volumes:
|
||||||
|
- ./salary_info.db:/app/salary_info.db:ro
|
||||||
|
- ./user_profile.db:/app/user_profile.db
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
@ -171,8 +171,10 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
|
|
||||||
const confirmLogout = () => {
|
const confirmLogout = () => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('id');
|
||||||
localStorage.removeItem('careerSuggestionsCache');
|
localStorage.removeItem('careerSuggestionsCache');
|
||||||
localStorage.removeItem('lastSelectedCareerProfileId');
|
localStorage.removeItem('lastSelectedCareerProfileId');
|
||||||
|
localStorage.removeItem('selectedCareer');
|
||||||
localStorage.removeItem('aiClickCount');
|
localStorage.removeItem('aiClickCount');
|
||||||
localStorage.removeItem('aiClickDate');
|
localStorage.removeItem('aiClickDate');
|
||||||
localStorage.removeItem('aiRecommendations');
|
localStorage.removeItem('aiRecommendations');
|
||||||
|
@ -511,8 +511,13 @@ useEffect(() => {
|
|||||||
const up = await authFetch('/api/user-profile');
|
const up = await authFetch('/api/user-profile');
|
||||||
if (up.ok) setUserProfile(await up.json());
|
if (up.ok) setUserProfile(await up.json());
|
||||||
|
|
||||||
const fp = await authFetch('api/premium/financial-profile');
|
const fp = await authFetch('/api/premium/financial-profile');
|
||||||
if (fp.ok) setFinancialProfile(await fp.json());
|
if (fp.status === 404) {
|
||||||
|
// user skipped onboarding – treat as empty object
|
||||||
|
setFinancialProfile({});
|
||||||
|
} else if (fp.ok) {
|
||||||
|
setFinancialProfile(await fp.json());
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -5,7 +5,12 @@ import { Button } from './ui/button.js';
|
|||||||
const Paywall = () => {
|
const Paywall = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { state } = useLocation();
|
const { state } = useLocation();
|
||||||
const { selectedCareer } = state || {};
|
|
||||||
|
const {
|
||||||
|
redirectTo = '/premium-onboarding', // wizard by default
|
||||||
|
prevState = {}, // any custom state we passed
|
||||||
|
selectedCareer
|
||||||
|
} = state || {};
|
||||||
|
|
||||||
const handleSubscribe = async () => {
|
const handleSubscribe = async () => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
@ -27,7 +32,7 @@ const Paywall = () => {
|
|||||||
if (user) window.dispatchEvent(new Event('user-updated')); // or your context setter
|
if (user) window.dispatchEvent(new Event('user-updated')); // or your context setter
|
||||||
|
|
||||||
// 2) give the auth context time to update, then push
|
// 2) give the auth context time to update, then push
|
||||||
navigate('/premium-onboarding', { replace: true, state: { selectedCareer } });
|
navigate(redirectTo, { replace: true, state: prevState });
|
||||||
} else {
|
} else {
|
||||||
console.error('activate-premium failed:', await res.text());
|
console.error('activate-premium failed:', await res.text());
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,38 @@
|
|||||||
// CareerOnboarding.js
|
// CareerOnboarding.js
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import axios from 'axios';
|
|
||||||
import { Input } from '../ui/input.js'; // Ensure path matches your structure
|
|
||||||
import authFetch from '../../utils/authFetch.js';
|
|
||||||
|
|
||||||
// 1) Import your CareerSearch component
|
// 1) Import your CareerSearch component
|
||||||
import CareerSearch from '../CareerSearch.js'; // adjust path as necessary
|
import CareerSearch from '../CareerSearch.js'; // adjust path as necessary
|
||||||
|
|
||||||
const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|
||||||
// We store local state for “are you working,” “selectedCareer,” etc.
|
|
||||||
const [currentlyWorking, setCurrentlyWorking] = useState('');
|
|
||||||
const [selectedCareer, setSelectedCareer] = useState('');
|
|
||||||
const [collegeEnrollmentStatus, setCollegeEnrollmentStatus] = useState('');
|
|
||||||
const [showFinPrompt, setShowFinPrompt] = useState(false);
|
|
||||||
const [financialReady, setFinancialReady] = useState(false); // persisted later if you wish
|
|
||||||
const Req = () => <span className="text-red-600 ml-0.5">*</span>;
|
const Req = () => <span className="text-red-600 ml-0.5">*</span>;
|
||||||
const ready = selectedCareer && currentlyWorking && collegeEnrollmentStatus;
|
|
||||||
|
|
||||||
|
const CareerOnboarding = ({ nextStep, prevStep, data, setData, finishNow }) => {
|
||||||
// 1) Grab the location state values, if any
|
// We store local state for “are you working,” “selectedCareer,” etc.
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navCareerObj = location.state?.selectedCareer;
|
||||||
|
const [careerObj, setCareerObj] = useState(() => {
|
||||||
|
if (navCareerObj) return navCareerObj;
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem('selectedCareer') || 'null');
|
||||||
|
} catch { return null; }
|
||||||
|
});
|
||||||
|
const [currentlyWorking, setCurrentlyWorking] = useState('');
|
||||||
|
const [collegeStatus, setCollegeStatus] = useState('');
|
||||||
|
const [showFinPrompt, setShowFinPrompt] = useState(false);
|
||||||
|
|
||||||
|
/* ── 2. derived helpers ───────────────────────────────────── */
|
||||||
|
const selectedCareerTitle = careerObj?.title || '';
|
||||||
|
const ready =
|
||||||
|
selectedCareerTitle &&
|
||||||
|
currentlyWorking &&
|
||||||
|
collegeStatus;
|
||||||
|
|
||||||
|
const inCollege = ['currently_enrolled', 'prospective_student']
|
||||||
|
.includes(collegeStatus);
|
||||||
|
const skipFin = !!data.skipFinancialStep;
|
||||||
|
// 1) Grab the location state values, if any
|
||||||
|
|
||||||
const {
|
const {
|
||||||
socCode,
|
socCode,
|
||||||
cipCodes,
|
cipCodes,
|
||||||
@ -29,63 +41,59 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
userState,
|
userState,
|
||||||
} = location.state || {};
|
} = location.state || {};
|
||||||
|
|
||||||
// 2) On mount, see if location.state has a careerTitle and update local states if needed
|
/* ── 3. side‑effects when route brings a new career object ── */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (careerTitle) {
|
if (!navCareerObj?.title) return;
|
||||||
setSelectedCareer(careerTitle);
|
|
||||||
setData((prev) => ({
|
setCareerObj(navCareerObj);
|
||||||
...prev,
|
localStorage.setItem('selectedCareer', JSON.stringify(navCareerObj));
|
||||||
career_name: careerTitle,
|
|
||||||
soc_code: socCode || ''
|
setData(prev => ({
|
||||||
}));
|
...prev,
|
||||||
}
|
career_name : navCareerObj.title,
|
||||||
}, [careerTitle, socCode, setData]);
|
soc_code : navCareerObj.soc_code || ''
|
||||||
|
}));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [navCareerObj]); // ← run once per navigation change
|
||||||
|
|
||||||
|
|
||||||
// Called whenever other <inputs> change
|
// Called whenever other <inputs> change
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Called when user picks a career from CareerSearch and confirms it
|
/* ── 4. callbacks ─────────────────────────────────────────── */
|
||||||
const handleCareerSelected = (careerObj) => {
|
function handleCareerSelected(obj) {
|
||||||
// e.g. { title, soc_code, cip_code, ... }
|
setCareerObj(obj);
|
||||||
setSelectedCareer(careerObj.title);
|
localStorage.setItem('selectedCareer', JSON.stringify(obj));
|
||||||
|
setData(prev => ({ ...prev, career_name: obj.title, soc_code: obj.soc_code || '' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (!ready) return alert('Fill all required fields.');
|
||||||
|
const inCollege = ['currently_enrolled', 'prospective_student'].includes(collegeStatus);
|
||||||
setData(prev => ({
|
setData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
career_name: careerObj.title,
|
career_name : selectedCareerTitle,
|
||||||
soc_code: careerObj.soc_code || '' // store SOC if needed
|
college_enrollment_status : collegeStatus,
|
||||||
|
currently_working : currentlyWorking,
|
||||||
|
inCollege,
|
||||||
}));
|
}));
|
||||||
};
|
/* — where do we go? — */
|
||||||
|
if (skipFin && !inCollege) {
|
||||||
const handleSubmit = () => {
|
/* user said “Skip” AND is not in college ⇒ jump to Review */
|
||||||
if (!selectedCareer || !currentlyWorking || !collegeEnrollmentStatus) {
|
finishNow(); // ← the helper we just injected via props
|
||||||
alert('Please complete all required fields before continuing.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isInCollege =
|
|
||||||
collegeEnrollmentStatus === 'currently_enrolled' ||
|
|
||||||
collegeEnrollmentStatus === 'prospective_student';
|
|
||||||
|
|
||||||
// Merge local state into parent data
|
|
||||||
setData(prevData => ({
|
|
||||||
...prevData,
|
|
||||||
career_name: selectedCareer,
|
|
||||||
college_enrollment_status: collegeEnrollmentStatus,
|
|
||||||
currently_working: currentlyWorking,
|
|
||||||
inCollege: isInCollege,
|
|
||||||
// fallback defaults, or use user-provided
|
|
||||||
status: prevData.status || 'planned',
|
|
||||||
start_date: prevData.start_date || new Date().toISOString().slice(0, 10).slice(0, 10),
|
|
||||||
projected_end_date: prevData.projected_end_date || null
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!showFinPrompt || financialReady) {
|
|
||||||
|
|
||||||
nextStep();
|
|
||||||
} else {
|
} else {
|
||||||
|
nextStep(); // ordinary flow
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const nextLabel = skipFin
|
||||||
|
? inCollege ? 'College →' : 'Finish →'
|
||||||
|
: inCollege ? 'College →' : 'Financial →';
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md mx-auto p-6 space-y-4">
|
<div className="max-w-md mx-auto p-6 space-y-4">
|
||||||
@ -116,18 +124,18 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
{/* 2) Replace old local “Search for Career” with <CareerSearch/> */}
|
{/* 2) Replace old local “Search for Career” with <CareerSearch/> */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="font-medium">
|
<h3 className="font-medium">
|
||||||
What career are you planning to pursue? (Please select from drop-down suggestions after typing)<Req />
|
Target Career <Req />
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
This should be your <strong>target career path</strong> — whether it’s a new goal or the one you're already in.
|
This should be the career you are <strong>striving for</strong> — whether it’s a new goal or the one you're already in.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CareerSearch onCareerSelected={handleCareerSelected} required />
|
<CareerSearch onCareerSelected={handleCareerSelected} required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedCareer && (
|
{selectedCareerTitle && (
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
Selected Career: <strong>{selectedCareer}</strong>
|
Selected Career: <strong>{selectedCareerTitle}</strong>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -173,9 +181,9 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
Are you currently enrolled in college or planning to enroll? <Req />
|
Are you currently enrolled in college or planning to enroll? <Req />
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={collegeEnrollmentStatus}
|
value={collegeStatus}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCollegeEnrollmentStatus(e.target.value);
|
setCollegeStatus(e.target.value);
|
||||||
setData(prev => ({ ...prev, college_enrollment_status: e.target.value }));
|
setData(prev => ({ ...prev, college_enrollment_status: e.target.value }));
|
||||||
const needsPrompt = ['currently_enrolled', 'prospective_student'].includes(e.target.value);
|
const needsPrompt = ['currently_enrolled', 'prospective_student'].includes(e.target.value);
|
||||||
setShowFinPrompt(needsPrompt);
|
setShowFinPrompt(needsPrompt);
|
||||||
@ -189,7 +197,7 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
<option value="prospective_student">Planning to Enroll (Prospective)</option>
|
<option value="prospective_student">Planning to Enroll (Prospective)</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{showFinPrompt && !financialReady && (
|
{showFinPrompt && (
|
||||||
<div className="mt-4 p-4 rounded border border-blue-300 bg-blue-50">
|
<div className="mt-4 p-4 rounded border border-blue-300 bg-blue-50">
|
||||||
<p className="text-sm mb-3">
|
<p className="text-sm mb-3">
|
||||||
We can give you step-by-step milestones right away. <br />
|
We can give you step-by-step milestones right away. <br />
|
||||||
@ -214,7 +222,6 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
/* mark intent to skip the finance step */
|
/* mark intent to skip the finance step */
|
||||||
setData(prev => ({ ...prev, skipFinancialStep: true }));
|
setData(prev => ({ ...prev, skipFinancialStep: true }));
|
||||||
|
|
||||||
setFinancialReady(false);
|
|
||||||
setShowFinPrompt(false); // hide the prompt, stay on page
|
setShowFinPrompt(false); // hide the prompt, stay on page
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -245,11 +252,11 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!ready}
|
disabled={!ready}
|
||||||
className={`py-2 px-4 rounded font-semibold
|
className={`py-2 px-4 rounded font-semibold
|
||||||
${selectedCareer && currentlyWorking && collegeEnrollmentStatus
|
${ready
|
||||||
? 'bg-blue-500 hover:bg-blue-600 text-white'
|
? 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
|
: 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
|
||||||
>
|
>
|
||||||
Financial →
|
{nextLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Modal from '../../components/ui/modal.js';
|
import Modal from '../../components/ui/modal.js';
|
||||||
import FinancialAidWizard from '../../components/FinancialAidWizard.js';
|
import FinancialAidWizard from '../../components/FinancialAidWizard.js';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
const Req = () => <span className="text-red-600 ml-0.5">*</span>;
|
||||||
|
|
||||||
function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||||
// CIP / iPEDS local states
|
// CIP / iPEDS local states
|
||||||
@ -11,9 +14,41 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
|
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
|
||||||
const [schoolValid, setSchoolValid] = useState(false);
|
const [schoolValid, setSchoolValid] = useState(false);
|
||||||
const [programValid, setProgramValid] = useState(false);
|
const [programValid, setProgramValid] = useState(false);
|
||||||
|
const [enrollmentDate, setEnrollmentDate] = useState(
|
||||||
|
data.enrollment_date || '' // carry forward if the user goes back
|
||||||
|
);
|
||||||
|
const [expectedGraduation, setExpectedGraduation] = useState(data.expected_graduation || '');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Show/hide the financial aid wizard
|
// Show/hide the financial aid wizard
|
||||||
const [showAidWizard, setShowAidWizard] = useState(false);
|
const [showAidWizard, setShowAidWizard] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const navSelectedSchoolRaw = location.state?.selectedSchool;
|
||||||
|
const navSelectedSchool = toSchoolName(navSelectedSchoolRaw);
|
||||||
|
|
||||||
|
|
||||||
|
function dehydrate(schObj) {
|
||||||
|
if (!schObj || typeof schObj !== 'object') return null;
|
||||||
|
/* keep only the fields you really need */
|
||||||
|
const { INSTNM, CIPDESC, CREDDESC, ...rest } = schObj;
|
||||||
|
return { INSTNM, CIPDESC, CREDDESC, ...rest };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [selectedSchool, setSelectedSchool] = useState(() =>
|
||||||
|
dehydrate(navSelectedSchool) ||
|
||||||
|
dehydrate(JSON.parse(localStorage.getItem('premiumOnboardingState') || '{}'
|
||||||
|
).collegeData?.selectedSchool)
|
||||||
|
);
|
||||||
|
|
||||||
|
function toSchoolName(objOrStr) {
|
||||||
|
if (!objOrStr) return '';
|
||||||
|
if (typeof objOrStr === 'object') return objOrStr.INSTNM || '';
|
||||||
|
return objOrStr; // already a string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const infoIcon = (msg) => (
|
const infoIcon = (msg) => (
|
||||||
<span
|
<span
|
||||||
@ -27,7 +62,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
// Destructure parent data
|
// Destructure parent data
|
||||||
const {
|
const {
|
||||||
college_enrollment_status = '',
|
college_enrollment_status = '',
|
||||||
selected_school = '',
|
selected_school = selectedSchool,
|
||||||
selected_program = '',
|
selected_program = '',
|
||||||
program_type = '',
|
program_type = '',
|
||||||
academic_calendar = 'semester',
|
academic_calendar = 'semester',
|
||||||
@ -53,10 +88,28 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
const [manualTuition, setManualTuition] = useState('');
|
const [manualTuition, setManualTuition] = useState('');
|
||||||
const [autoTuition, setAutoTuition] = useState(0);
|
const [autoTuition, setAutoTuition] = useState(0);
|
||||||
const [manualProgramLength, setManualProgramLength] = useState('');
|
const [manualProgramLength, setManualProgramLength] = useState('');
|
||||||
const [autoProgramLength, setAutoProgramLength] = useState('0.00');
|
const [autoProgramLength, setAutoProgramLength] = useState(0);
|
||||||
|
|
||||||
|
const inSchool = ['currently_enrolled','prospective_student']
|
||||||
|
.includes(college_enrollment_status);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedSchool) {
|
||||||
|
setData(prev => ({
|
||||||
|
...prev,
|
||||||
|
selected_school : selectedSchool.INSTNM,
|
||||||
|
selected_program: selectedSchool.CIPDESC || prev.selected_program,
|
||||||
|
program_type : selectedSchool.CREDDESC || prev.program_type
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [selectedSchool, setData]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.expected_graduation && !expectedGraduation)
|
||||||
|
setExpectedGraduation(data.expected_graduation);
|
||||||
|
}, [data.expected_graduation]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handleParentFieldChange
|
* handleParentFieldChange
|
||||||
* If user leaves numeric fields blank, store '' in local state, not 0.
|
* If user leaves numeric fields blank, store '' in local state, not 0.
|
||||||
@ -144,9 +197,24 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
fetchIpedsData();
|
fetchIpedsData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (college_enrollment_status !== 'prospective_student') return;
|
||||||
|
|
||||||
|
const lenYears = Number(data.program_length || '');
|
||||||
|
if (!enrollmentDate || !lenYears) return;
|
||||||
|
|
||||||
|
const start = new Date(enrollmentDate);
|
||||||
|
const est = new Date(start.getFullYear() + lenYears, start.getMonth(), start.getDate());
|
||||||
|
const iso = firstOfNextMonth(est);
|
||||||
|
|
||||||
|
setExpectedGraduation(iso);
|
||||||
|
setData(prev => ({ ...prev, expected_graduation: iso }));
|
||||||
|
}, [college_enrollment_status, enrollmentDate, data.program_length, setData]);
|
||||||
|
|
||||||
// School Name
|
// School Name
|
||||||
const handleSchoolChange = (e) => {
|
const handleSchoolChange = (eOrVal) => {
|
||||||
const value = e.target.value;
|
const value =
|
||||||
|
typeof eOrVal === 'string' ? eOrVal : eOrVal?.target?.value || '';
|
||||||
setData(prev => ({
|
setData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
selected_school: value,
|
selected_school: value,
|
||||||
@ -312,14 +380,49 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
const remain = Math.max(0, required - completed);
|
const remain = Math.max(0, required - completed);
|
||||||
const yrs = remain / perYear;
|
const yrs = remain / perYear;
|
||||||
|
|
||||||
setAutoProgramLength(yrs.toFixed(2));
|
setAutoProgramLength(parseFloat(yrs.toFixed(2)));
|
||||||
}, [
|
}, [
|
||||||
program_type,
|
program_type,
|
||||||
hours_completed,
|
hours_completed,
|
||||||
credit_hours_per_year,
|
credit_hours_per_year,
|
||||||
credit_hours_required
|
credit_hours_required,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Whenever the user changes enrollmentDate OR programLength */
|
||||||
|
/* (program_length is already in parent data), compute grad date. */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
useEffect(() => {
|
||||||
|
/* decide which “length” the user is looking at right now */
|
||||||
|
const lenRaw =
|
||||||
|
manualProgramLength.trim() !== ''
|
||||||
|
? manualProgramLength
|
||||||
|
: autoProgramLength;
|
||||||
|
|
||||||
|
const len = parseFloat(lenRaw); // years (may be fractional)
|
||||||
|
const startISO = pickStartDate(); // '' or yyyy‑mm‑dd
|
||||||
|
if (!startISO || !len) return; // nothing to do yet
|
||||||
|
|
||||||
|
const start = new Date(startISO);
|
||||||
|
/* naïve add – assuming program_length is years; *
|
||||||
|
* adjust if you store months instead */
|
||||||
|
/* 1 year = 12 months ‑‑ preserve fractions (e.g. 1.75 y = 21 m) */
|
||||||
|
const monthsToAdd = Math.round(len * 12);
|
||||||
|
|
||||||
|
const estGrad = new Date(start); // clone
|
||||||
|
estGrad.setMonth(estGrad.getMonth() + monthsToAdd);
|
||||||
|
|
||||||
|
const gradISO = firstOfNextMonth(estGrad);
|
||||||
|
|
||||||
|
setExpectedGraduation(gradISO);
|
||||||
|
setData(prev => ({ ...prev, expected_graduation: gradISO }));
|
||||||
|
}, [college_enrollment_status,
|
||||||
|
enrollmentDate,
|
||||||
|
manualProgramLength,
|
||||||
|
autoProgramLength,
|
||||||
|
setData]);
|
||||||
|
|
||||||
|
|
||||||
// final handleSubmit => we store chosen tuition + program_length, then move on
|
// final handleSubmit => we store chosen tuition + program_length, then move on
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const chosenTuition = manualTuition.trim() === ''
|
const chosenTuition = manualTuition.trim() === ''
|
||||||
@ -342,12 +445,26 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition);
|
const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition);
|
||||||
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
||||||
|
|
||||||
const ready =
|
function pickStartDate() {
|
||||||
(college_enrollment_status === 'currently_enrolled' ||
|
if (college_enrollment_status === 'prospective_student') {
|
||||||
college_enrollment_status === 'prospective_student')
|
return enrollmentDate; // may still be ''
|
||||||
? schoolValid && programValid && program_type
|
}
|
||||||
: true;
|
if (college_enrollment_status === 'currently_enrolled') {
|
||||||
|
return firstOfNextMonth(new Date()); // today → 1st next month
|
||||||
|
}
|
||||||
|
return ''; // anybody else
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstOfNextMonth(dateObj) {
|
||||||
|
return new Date(dateObj.getFullYear(), dateObj.getMonth() + 1, 1)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 10); // yyyy‑mm‑dd
|
||||||
|
}
|
||||||
|
|
||||||
|
const ready =
|
||||||
|
(!inSchool || expectedGraduation) && // grad date iff in school
|
||||||
|
selected_school && program_type;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md mx-auto p-6 space-y-4">
|
<div className="max-w-md mx-auto p-6 space-y-4">
|
||||||
<h2 className="text-2xl font-semibold">College Details</h2>
|
<h2 className="text-2xl font-semibold">College Details</h2>
|
||||||
@ -601,16 +718,53 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-1">
|
{['currently_enrolled','prospective_student'].includes(college_enrollment_status) && (
|
||||||
<label className="block font-medium">Expected Graduation {infoIcon("If you don't know the exact date, that's fine - just enter the targeted month")}</label>
|
<>
|
||||||
<input
|
{/* A) Enrollment date – prospective only */}
|
||||||
type="date"
|
{college_enrollment_status === 'prospective_student' && (
|
||||||
name="expected_graduation"
|
<div className="space-y-2">
|
||||||
value={expected_graduation}
|
<label className="block font-medium">
|
||||||
onChange={handleParentFieldChange}
|
Anticipated Enrollment Date <Req />
|
||||||
className="w-full border rounded p-2"
|
</label>
|
||||||
/>
|
<input
|
||||||
</div>
|
type="date"
|
||||||
|
value={enrollmentDate}
|
||||||
|
onChange={e => {
|
||||||
|
setEnrollmentDate(e.target.value);
|
||||||
|
setData(p => ({ ...p, enrollment_date: e.target.value }));
|
||||||
|
}}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* B) Expected graduation – always editable */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block font-medium">
|
||||||
|
Expected Graduation Date <Req />
|
||||||
|
{college_enrollment_status === 'prospective_student' &&
|
||||||
|
enrollmentDate && data.program_length && (
|
||||||
|
<span
|
||||||
|
className="ml-1 cursor-help text-blue-600"
|
||||||
|
title="Automatically estimated from your enrollment date and program length. Adjust if needed—actual calendars vary by institution."
|
||||||
|
>ⓘ</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={expectedGraduation}
|
||||||
|
onChange={e => {
|
||||||
|
setExpectedGraduation(e.target.value);
|
||||||
|
setData(p => ({ ...p, expected_graduation: e.target.value }));
|
||||||
|
}}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="block font-medium">Loan Interest Rate (%) {infoIcon("These can vary, enter the best blended rate you can approximate if you have multiple.")}</label>
|
<label className="block font-medium">Loan Interest Rate (%) {infoIcon("These can vary, enter the best blended rate you can approximate if you have multiple.")}</label>
|
||||||
|
@ -86,6 +86,12 @@ const OnboardingContainer = () => {
|
|||||||
console.log('Current careerData:', careerData);
|
console.log('Current careerData:', careerData);
|
||||||
console.log('Current collegeData:', collegeData);
|
console.log('Current collegeData:', collegeData);
|
||||||
|
|
||||||
|
|
||||||
|
function finishImmediately() {
|
||||||
|
// The review page is the last item in the steps array ⇒ index = onboardingSteps.length‑1
|
||||||
|
setStep(onboardingSteps.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Final “all done” submission
|
// 4. Final “all done” submission
|
||||||
const handleFinalSubmit = async () => {
|
const handleFinalSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
@ -209,6 +215,7 @@ navigate(`/career-roadmap/${finalCareerProfileId}`, {
|
|||||||
|
|
||||||
<CareerOnboarding
|
<CareerOnboarding
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
|
finishNow={finishImmediately}
|
||||||
data={careerData}
|
data={careerData}
|
||||||
setData={setCareerData}
|
setData={setCareerData}
|
||||||
/>,
|
/>,
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
import React from 'react';
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
import { Navigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
function PremiumRoute({ user, children }) {
|
export default function PremiumRoute({ user, children }) {
|
||||||
if (!user) {
|
const loc = useLocation();
|
||||||
// Not even logged in; go to sign in
|
|
||||||
return <Navigate to="/signin" replace />;
|
/* Already premium → proceed */
|
||||||
|
if (user?.is_premium || user?.is_pro_premium) {
|
||||||
|
return children;
|
||||||
}
|
}
|
||||||
|
/* NEW: send to paywall and remember where they wanted to go */
|
||||||
// Check if user has *either* premium or pro
|
return (
|
||||||
const hasPremiumOrPro = user.is_premium || user.is_pro_premium;
|
<Navigate
|
||||||
if (!hasPremiumOrPro) {
|
to="/paywall"
|
||||||
// Logged in but neither plan; go to paywall
|
replace
|
||||||
return <Navigate to="/paywall" replace />;
|
state={{ redirectTo: loc.pathname, prevState: loc.state }}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
// User is logged in and has premium or pro
|
}
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PremiumRoute;
|
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user