Pulled in selected values for premium onboarding, added Expected Graduation Date calculation
This commit is contained in:
parent
437a140e9a
commit
ef290a96ea
@ -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
|
||||
const salaryDbPath =
|
||||
process.env.SALARY_DB || '/app/data/salary_info.db';
|
||||
|
||||
const salaryDbPath =
|
||||
process.env.SALARY_DB_PATH // ← preferred
|
||||
|| process.env.SALARY_DB // ← legacy
|
||||
|| '/app/salary_info.db'; // final fallback
|
||||
|
||||
const salaryDb = new sqlite3.Database(
|
||||
salaryDbPath,
|
||||
sqlite3.OPEN_READONLY,
|
@ -2452,18 +2452,30 @@ app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, asyn
|
||||
FINANCIAL PROFILES
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT *
|
||||
FROM financial_profiles
|
||||
WHERE user_id = ?
|
||||
`, [req.id]);
|
||||
res.json(rows[0] || {});
|
||||
} catch (error) {
|
||||
console.error('Error fetching financial profile:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch financial profile' });
|
||||
}
|
||||
// GET /api/premium/financial-profile
|
||||
app.get('/api/premium/financial-profile', auth, (req, res) => {
|
||||
const uid = req.userId;
|
||||
db.query('SELECT * FROM financial_profile WHERE user_id=?', [uid],
|
||||
(err, rows) => {
|
||||
if (err) return res.status(500).json({ error:'DB error' });
|
||||
|
||||
if (!rows.length) {
|
||||
// ←———— send a benign default instead of 404
|
||||
return res.json({
|
||||
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) => {
|
||||
|
@ -8,6 +8,11 @@ services:
|
||||
<<: *with-env
|
||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG}
|
||||
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:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"]
|
||||
interval: 30s
|
||||
|
@ -171,8 +171,10 @@ const uiToolHandlers = useMemo(() => {
|
||||
|
||||
const confirmLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('id');
|
||||
localStorage.removeItem('careerSuggestionsCache');
|
||||
localStorage.removeItem('lastSelectedCareerProfileId');
|
||||
localStorage.removeItem('selectedCareer');
|
||||
localStorage.removeItem('aiClickCount');
|
||||
localStorage.removeItem('aiClickDate');
|
||||
localStorage.removeItem('aiRecommendations');
|
||||
|
@ -511,8 +511,13 @@ useEffect(() => {
|
||||
const up = await authFetch('/api/user-profile');
|
||||
if (up.ok) setUserProfile(await up.json());
|
||||
|
||||
const fp = await authFetch('api/premium/financial-profile');
|
||||
if (fp.ok) setFinancialProfile(await fp.json());
|
||||
const fp = await authFetch('/api/premium/financial-profile');
|
||||
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 navigate = useNavigate();
|
||||
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 token = localStorage.getItem('token');
|
||||
@ -27,7 +32,7 @@ const Paywall = () => {
|
||||
if (user) window.dispatchEvent(new Event('user-updated')); // or your context setter
|
||||
|
||||
// 2) give the auth context time to update, then push
|
||||
navigate('/premium-onboarding', { replace: true, state: { selectedCareer } });
|
||||
navigate(redirectTo, { replace: true, state: prevState });
|
||||
} else {
|
||||
console.error('activate-premium failed:', await res.text());
|
||||
}
|
||||
|
@ -1,26 +1,38 @@
|
||||
// CareerOnboarding.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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
|
||||
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 ready = selectedCareer && currentlyWorking && collegeEnrollmentStatus;
|
||||
|
||||
|
||||
// 1) Grab the location state values, if any
|
||||
const CareerOnboarding = ({ nextStep, prevStep, data, setData, finishNow }) => {
|
||||
// We store local state for “are you working,” “selectedCareer,” etc.
|
||||
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 {
|
||||
socCode,
|
||||
cipCodes,
|
||||
@ -29,63 +41,59 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||
userState,
|
||||
} = 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(() => {
|
||||
if (careerTitle) {
|
||||
setSelectedCareer(careerTitle);
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
career_name: careerTitle,
|
||||
soc_code: socCode || ''
|
||||
}));
|
||||
}
|
||||
}, [careerTitle, socCode, setData]);
|
||||
if (!navCareerObj?.title) return;
|
||||
|
||||
setCareerObj(navCareerObj);
|
||||
localStorage.setItem('selectedCareer', JSON.stringify(navCareerObj));
|
||||
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
career_name : navCareerObj.title,
|
||||
soc_code : navCareerObj.soc_code || ''
|
||||
}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navCareerObj]); // ← run once per navigation change
|
||||
|
||||
|
||||
// Called whenever other <inputs> change
|
||||
const handleChange = (e) => {
|
||||
setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
// Called when user picks a career from CareerSearch and confirms it
|
||||
const handleCareerSelected = (careerObj) => {
|
||||
// e.g. { title, soc_code, cip_code, ... }
|
||||
setSelectedCareer(careerObj.title);
|
||||
/* ── 4. callbacks ─────────────────────────────────────────── */
|
||||
function handleCareerSelected(obj) {
|
||||
setCareerObj(obj);
|
||||
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 => ({
|
||||
...prev,
|
||||
career_name: careerObj.title,
|
||||
soc_code: careerObj.soc_code || '' // store SOC if needed
|
||||
career_name : selectedCareerTitle,
|
||||
college_enrollment_status : collegeStatus,
|
||||
currently_working : currentlyWorking,
|
||||
inCollege,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!selectedCareer || !currentlyWorking || !collegeEnrollmentStatus) {
|
||||
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();
|
||||
/* — where do we go? — */
|
||||
if (skipFin && !inCollege) {
|
||||
/* user said “Skip” AND is not in college ⇒ jump to Review */
|
||||
finishNow(); // ← the helper we just injected via props
|
||||
} else {
|
||||
|
||||
nextStep(); // ordinary flow
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const nextLabel = skipFin
|
||||
? inCollege ? 'College →' : 'Finish →'
|
||||
: inCollege ? 'College →' : 'Financial →';
|
||||
|
||||
|
||||
return (
|
||||
<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/> */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium">
|
||||
What career are you planning to pursue? (Please select from drop-down suggestions after typing)<Req />
|
||||
Target Career <Req />
|
||||
</h3>
|
||||
<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>
|
||||
|
||||
<CareerSearch onCareerSelected={handleCareerSelected} required />
|
||||
</div>
|
||||
|
||||
{selectedCareer && (
|
||||
{selectedCareerTitle && (
|
||||
<p className="text-gray-700">
|
||||
Selected Career: <strong>{selectedCareer}</strong>
|
||||
Selected Career: <strong>{selectedCareerTitle}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -173,9 +181,9 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||
Are you currently enrolled in college or planning to enroll? <Req />
|
||||
</label>
|
||||
<select
|
||||
value={collegeEnrollmentStatus}
|
||||
value={collegeStatus}
|
||||
onChange={(e) => {
|
||||
setCollegeEnrollmentStatus(e.target.value);
|
||||
setCollegeStatus(e.target.value);
|
||||
setData(prev => ({ ...prev, college_enrollment_status: e.target.value }));
|
||||
const needsPrompt = ['currently_enrolled', 'prospective_student'].includes(e.target.value);
|
||||
setShowFinPrompt(needsPrompt);
|
||||
@ -189,7 +197,7 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||
<option value="prospective_student">Planning to Enroll (Prospective)</option>
|
||||
</select>
|
||||
|
||||
{showFinPrompt && !financialReady && (
|
||||
{showFinPrompt && (
|
||||
<div className="mt-4 p-4 rounded border border-blue-300 bg-blue-50">
|
||||
<p className="text-sm mb-3">
|
||||
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 */
|
||||
setData(prev => ({ ...prev, skipFinancialStep: true }));
|
||||
|
||||
setFinancialReady(false);
|
||||
setShowFinPrompt(false); // hide the prompt, stay on page
|
||||
}}
|
||||
>
|
||||
@ -245,11 +252,11 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||
onClick={handleSubmit}
|
||||
disabled={!ready}
|
||||
className={`py-2 px-4 rounded font-semibold
|
||||
${selectedCareer && currentlyWorking && collegeEnrollmentStatus
|
||||
${ready
|
||||
? 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
|
||||
>
|
||||
Financial →
|
||||
{nextLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Modal from '../../components/ui/modal.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 }) {
|
||||
// CIP / iPEDS local states
|
||||
@ -11,9 +14,41 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
|
||||
const [schoolValid, setSchoolValid] = 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
|
||||
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) => (
|
||||
<span
|
||||
@ -27,7 +62,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
// Destructure parent data
|
||||
const {
|
||||
college_enrollment_status = '',
|
||||
selected_school = '',
|
||||
selected_school = selectedSchool,
|
||||
selected_program = '',
|
||||
program_type = '',
|
||||
academic_calendar = 'semester',
|
||||
@ -53,10 +88,28 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
const [manualTuition, setManualTuition] = useState('');
|
||||
const [autoTuition, setAutoTuition] = useState(0);
|
||||
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
|
||||
* If user leaves numeric fields blank, store '' in local state, not 0.
|
||||
@ -144,9 +197,24 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
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
|
||||
const handleSchoolChange = (e) => {
|
||||
const value = e.target.value;
|
||||
const handleSchoolChange = (eOrVal) => {
|
||||
const value =
|
||||
typeof eOrVal === 'string' ? eOrVal : eOrVal?.target?.value || '';
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
selected_school: value,
|
||||
@ -312,14 +380,49 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
const remain = Math.max(0, required - completed);
|
||||
const yrs = remain / perYear;
|
||||
|
||||
setAutoProgramLength(yrs.toFixed(2));
|
||||
setAutoProgramLength(parseFloat(yrs.toFixed(2)));
|
||||
}, [
|
||||
program_type,
|
||||
hours_completed,
|
||||
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
|
||||
const handleSubmit = () => {
|
||||
const chosenTuition = manualTuition.trim() === ''
|
||||
@ -342,12 +445,26 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition);
|
||||
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
||||
|
||||
const ready =
|
||||
(college_enrollment_status === 'currently_enrolled' ||
|
||||
college_enrollment_status === 'prospective_student')
|
||||
? schoolValid && programValid && program_type
|
||||
: true;
|
||||
function pickStartDate() {
|
||||
if (college_enrollment_status === 'prospective_student') {
|
||||
return enrollmentDate; // may still be ''
|
||||
}
|
||||
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 (
|
||||
<div className="max-w-md mx-auto p-6 space-y-4">
|
||||
<h2 className="text-2xl font-semibold">College Details</h2>
|
||||
@ -601,16 +718,53 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<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
|
||||
type="date"
|
||||
name="expected_graduation"
|
||||
value={expected_graduation}
|
||||
onChange={handleParentFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
{['currently_enrolled','prospective_student'].includes(college_enrollment_status) && (
|
||||
<>
|
||||
{/* A) Enrollment date – prospective only */}
|
||||
{college_enrollment_status === 'prospective_student' && (
|
||||
<div className="space-y-2">
|
||||
<label className="block font-medium">
|
||||
Anticipated Enrollment Date <Req />
|
||||
</label>
|
||||
<input
|
||||
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">
|
||||
<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 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
|
||||
const handleFinalSubmit = async () => {
|
||||
try {
|
||||
@ -209,6 +215,7 @@ navigate(`/career-roadmap/${finalCareerProfileId}`, {
|
||||
|
||||
<CareerOnboarding
|
||||
nextStep={nextStep}
|
||||
finishNow={finishImmediately}
|
||||
data={careerData}
|
||||
setData={setCareerData}
|
||||
/>,
|
||||
|
@ -1,21 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
function PremiumRoute({ user, children }) {
|
||||
if (!user) {
|
||||
// Not even logged in; go to sign in
|
||||
return <Navigate to="/signin" replace />;
|
||||
export default function PremiumRoute({ user, children }) {
|
||||
const loc = useLocation();
|
||||
|
||||
/* Already premium → proceed */
|
||||
if (user?.is_premium || user?.is_pro_premium) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// Check if user has *either* premium or pro
|
||||
const hasPremiumOrPro = user.is_premium || user.is_pro_premium;
|
||||
if (!hasPremiumOrPro) {
|
||||
// Logged in but neither plan; go to paywall
|
||||
return <Navigate to="/paywall" replace />;
|
||||
}
|
||||
|
||||
// User is logged in and has premium or pro
|
||||
return children;
|
||||
}
|
||||
|
||||
export default PremiumRoute;
|
||||
/* NEW: send to paywall and remember where they wanted to go */
|
||||
return (
|
||||
<Navigate
|
||||
to="/paywall"
|
||||
replace
|
||||
state={{ redirectTo: loc.pathname, prevState: loc.state }}
|
||||
/>
|
||||
);
|
||||
}
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user