diff --git a/Dockerfile.server b/Dockerfile.server deleted file mode 100644 index 14429c4..0000000 --- a/Dockerfile.server +++ /dev/null @@ -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"] diff --git a/Dockerfile.server1 b/Dockerfile.server1 new file mode 100644 index 0000000..39634b6 --- /dev/null +++ b/Dockerfile.server1 @@ -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"] \ No newline at end of file diff --git a/backend/server.js b/backend/server1.js similarity index 95% rename from backend/server.js rename to backend/server1.js index ed25ad8..b6f34c4 100755 --- a/backend/server.js +++ b/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, diff --git a/backend/server3.js b/backend/server3.js index 4cce5f6..a62a27d 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -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) => { diff --git a/docker-compose.yml b/docker-compose.yml index 3180b07..db7ce11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/src/App.js b/src/App.js index 671103f..84ddf58 100644 --- a/src/App.js +++ b/src/App.js @@ -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'); diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js index ba7ebe9..8417dfd 100644 --- a/src/components/CareerRoadmap.js +++ b/src/components/CareerRoadmap.js @@ -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()); + } })(); }, []); diff --git a/src/components/Paywall.js b/src/components/Paywall.js index e79b6eb..dc2b827 100644 --- a/src/components/Paywall.js +++ b/src/components/Paywall.js @@ -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()); } diff --git a/src/components/PremiumOnboarding/CareerOnboarding.js b/src/components/PremiumOnboarding/CareerOnboarding.js index 35396ea..cda102a 100644 --- a/src/components/PremiumOnboarding/CareerOnboarding.js +++ b/src/components/PremiumOnboarding/CareerOnboarding.js @@ -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 = () => *; - 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 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 (
@@ -116,18 +124,18 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => { {/* 2) Replace old local “Search for Career” with */}

- What career are you planning to pursue? (Please select from drop-down suggestions after typing) + Target Career

- This should be your target career path — whether it’s a new goal or the one you're already in. + This should be the career you are striving for — whether it’s a new goal or the one you're already in.

- {selectedCareer && ( + {selectedCareerTitle && (

- Selected Career: {selectedCareer} + Selected Career: {selectedCareerTitle}

)} @@ -173,9 +181,9 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => { Are you currently enrolled in college or planning to enroll? - {showFinPrompt && !financialReady && ( + {showFinPrompt && (

We can give you step-by-step milestones right away.
@@ -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}

diff --git a/src/components/PremiumOnboarding/CollegeOnboarding.js b/src/components/PremiumOnboarding/CollegeOnboarding.js index 1c6e99a..d9e63d9 100644 --- a/src/components/PremiumOnboarding/CollegeOnboarding.js +++ b/src/components/PremiumOnboarding/CollegeOnboarding.js @@ -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 = () => *; 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) => ( { + 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 (

College Details

@@ -601,16 +718,53 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
)} -
- - -
+ {['currently_enrolled','prospective_student'].includes(college_enrollment_status) && ( + <> + {/* A) Enrollment date – prospective only */} + {college_enrollment_status === 'prospective_student' && ( +
+ + { + setEnrollmentDate(e.target.value); + setData(p => ({ ...p, enrollment_date: e.target.value })); + }} + className="w-full border rounded p-2" + required + /> +
+ )} + + {/* B) Expected graduation – always editable */} +
+ + + { + setExpectedGraduation(e.target.value); + setData(p => ({ ...p, expected_graduation: e.target.value })); + }} + className="w-full border rounded p-2" + required + /> +
+ + )}
diff --git a/src/components/PremiumOnboarding/OnboardingContainer.js b/src/components/PremiumOnboarding/OnboardingContainer.js index 9c68e53..2f6c780 100644 --- a/src/components/PremiumOnboarding/OnboardingContainer.js +++ b/src/components/PremiumOnboarding/OnboardingContainer.js @@ -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}`, { , diff --git a/src/components/PremiumRoute.js b/src/components/PremiumRoute.js index 649fbf9..27dc219 100644 --- a/src/components/PremiumRoute.js +++ b/src/components/PremiumRoute.js @@ -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 ; +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 ; - } - - // 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 ( + + ); +} \ No newline at end of file diff --git a/user_profile.db b/user_profile.db index 051a0bf..cb40bcc 100644 Binary files a/user_profile.db and b/user_profile.db differ