diff --git a/backend/server3.js b/backend/server3.js
index 72b64ac..1ecc3e5 100644
--- a/backend/server3.js
+++ b/backend/server3.js
@@ -259,6 +259,19 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
}
});
+// server3.js (add near the other career-profile routes)
+app.put('/api/premium/career-profile/:id/goals', authenticatePremiumUser, async (req, res) => {
+ const { id } = req.params;
+ const { career_goals } = req.body;
+
+ // simple ownership check
+ const [rows] = await pool.query('SELECT user_id FROM career_profiles WHERE id=?', [id]);
+ if (!rows[0] || rows[0].user_id !== req.id) {
+ return res.status(403).json({ error: 'Not your profile.' });
+ }
+ await pool.query('UPDATE career_profiles SET career_goals=? WHERE id=?', [career_goals, id]);
+ res.json({ career_goals });
+});
// DELETE a career profile (scenario) by ID
app.delete('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => {
@@ -2473,6 +2486,40 @@ app.delete('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, re
}
});
+app.get('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
+ try {
+ const { career_path_id, status = 'all' } = req.query;
+ const args = [req.id]; // << first placeholder = user_id
+
+ let sql = `
+ SELECT
+ t.id, t.milestone_id, t.title, t.description,
+ t.due_date, t.status,
+ t.created_at, t.updated_at,
+
+ m.title AS milestone_title,
+ m.date AS milestone_date,
+ cp.id AS career_path_id,
+ cp.career_name
+ FROM tasks t
+ JOIN milestones m ON m.id = t.milestone_id
+ JOIN career_paths cp ON cp.id = m.career_path_id
+ WHERE cp.user_id = ?
+ `;
+
+ if (career_path_id) { sql += ' AND cp.id = ?'; args.push(career_path_id); }
+ if (status !== 'all') { sql += ' AND t.status = ?'; args.push(status); }
+
+ sql += ' ORDER BY COALESCE(t.due_date, m.date) ASC';
+
+ const [rows] = await pool.query(sql, args);
+ return res.json(rows);
+ } catch (err) {
+ console.error('Error fetching tasks:', err);
+ return res.status(500).json({ error: 'Failed to fetch tasks.' });
+ }
+});
+
/* ------------------------------------------------------------------
MILESTONE IMPACTS ENDPOINTS
------------------------------------------------------------------ */
@@ -2664,6 +2711,10 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser,
}
});
+/* ------------------------------------------------------------------
+ O*NET KSA DATA
+ ------------------------------------------------------------------ */
+
let onetKsaData = []; // entire array from ksa_data.json
let allKsaNames = []; // an array of unique KSA names (for fuzzy matching)
diff --git a/src/components/CareerCoach.js b/src/components/CareerCoach.js
index 4124e0a..d2393a1 100644
--- a/src/components/CareerCoach.js
+++ b/src/components/CareerCoach.js
@@ -73,6 +73,8 @@ export default function CareerCoach({
userProfile,
financialProfile,
scenarioRow,
+ setScenarioRow,
+ careerProfileId,
collegeProfile,
onMilestonesCreated,
onAiRiskFetched
@@ -83,6 +85,9 @@ export default function CareerCoach({
const [loading, setLoading] = useState(false);
const [aiRisk, setAiRisk] = useState(null);
const chatRef = useRef(null);
+ const [showGoals , setShowGoals ] = useState(false);
+ const [draftGoals, setDraftGoals] = useState(scenarioRow?.career_goals || "");
+ const [saving , setSaving ] = useState(false);
/* -------------- scroll --------------- */
useEffect(() => {
@@ -248,7 +253,7 @@ I'm here to support you with personalized coaching. What would you like to focus
Career Coach
{/* Quick-action bar */}
-
+
-
+ {/* pushes Edit Goals to the far right */}
+
+
+
+
+
{/* Chat area */}
+
+ {showGoals && (
+
+
+
Edit Your Career Goals
+
+
+
+)}
+
);
}
diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js
index 75a6307..9fb3ea2 100644
--- a/src/components/CareerRoadmap.js
+++ b/src/components/CareerRoadmap.js
@@ -18,6 +18,7 @@ import {
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import MilestonePanel from './MilestonePanel.js';
+import MilestoneDrawer from './MilestoneDrawer.js';
import MilestoneEditModal from './MilestoneEditModal.js';
import buildChartMarkers from '../utils/buildChartMarkers.js';
import getMissingFields from '../utils/getMissingFields.js';
@@ -353,6 +354,9 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
const [projectionData, setProjectionData] = useState([]);
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
const [milestoneForModal, setMilestoneForModal] = useState(null);
+ const [drawerOpen, setDrawerOpen] = useState(false);
+ const [focusMid , setFocusMid ] = useState(null);
+ const [drawerMilestone, setDrawerMilestone] = useState(null);
// Config
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
@@ -558,18 +562,18 @@ useEffect(() => {
const dataReady = !!scenarioRow && !!financialProfile && collegeProfile !== null;
useEffect(() => {
- const guard = modalGuard.current;
- if (!dataReady || guard.checked) return;
- /* honour skip flag (one-time after onboarding / sessionStorage) */
- if (guard.skip) {
- guard.skip = false; // consume it
- guard.checked = true;
- return;
- }
+ if (!dataReady || !careerProfileId) return;
+
+ // one key per career profile
+ const key = `modalChecked:${careerProfileId}`;
+
+ // already checked in this browser session?
+ if (sessionStorage.getItem(key) === '1') return;
const status = (scenarioRow.college_enrollment_status || '').toLowerCase();
- const requireCollege = ['currently_enrolled', 'prospective_student', 'deferred'].includes(status);
+ const requireCollege = ['currently_enrolled','prospective_student','deferred']
+ .includes(status);
const missing = getMissingFields(
{ scenario: scenarioRow, financial: financialProfile, college: collegeProfile },
@@ -577,8 +581,11 @@ useEffect(() => {
);
if (missing.length) setShowEditModal(true);
- guard.checked = true; // ensure we don’t rerun
-}, [dataReady, scenarioRow, financialProfile, collegeProfile]);
+
+ sessionStorage.setItem(key, '1'); // remember for this tab
+
+}, [dataReady, scenarioRow, financialProfile, collegeProfile, careerProfileId]);
+
useEffect(() => {
@@ -1199,6 +1206,8 @@ const fetchMilestones = useCallback(async () => {
userProfile={userProfile}
financialProfile={financialProfile}
scenarioRow={scenarioRow}
+ setScenarioRow={setScenarioRow}
+ careerProfileId={careerProfileId}
collegeProfile={collegeProfile}
onMilestonesCreated={() => {
/* refresh or reload logic here */
@@ -1326,61 +1335,65 @@ const fetchMilestones = useCallback(async () => {
*/}
{/* --- FINANCIAL PROJECTION SECTION -------------------------------- */}
-
-
Financial Projection
+
+
Financial Projection
- {projectionData.length ? (
-
- {/* Milestone list / editor */}
-
+ {projectionData.length ? (
+
+ {/* Chart – now full width */}
+
+ p.month),
+ datasets: chartDatasets
+ }}
+ options={{
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { position: 'bottom' },
+ tooltip: { mode: 'index', intersect: false },
+ annotation: { annotations: allAnnotations }, // ✅ new
+ zoom: zoomConfig
+ },
+ scales: xAndYScales // unchanged
+ }}
+ />
+
- {/* Chart */}
-
-
-
p.month),
- datasets: chartDatasets
- }}
- options={{
- maintainAspectRatio: false,
- plugins: {
- legend: { position: 'bottom' },
- tooltip: { mode: 'index', intersect: false },
- annotation: { annotations: allAnnotations }, // ✅ new
- zoom: zoomConfig
- },
- scales: xAndYScales // unchanged
- }}
- />
+
+
+ {loanPayoffMonth && hasStudentLoan && (
+
+ Loan Paid Off:
+ {loanPayoffMonth}
+
+ )}
+
+
+ ) : (
+
No financial projection data found.
+ )}
-
+ {/* Milestones – stacked list under chart */}
+
+
Milestones
+
{
+ setDrawerMilestone(m);
+ setDrawerOpen(true);
+ }}
+ />
- {loanPayoffMonth && hasStudentLoan && (
-
- Loan Paid Off:
- {loanPayoffMonth}
-
- )}
-
-
-
- ) : (
-
No financial projection data found.
- )}
-
+
{/* 6) Simulation length + Edit scenario */}
@@ -1474,6 +1487,17 @@ const fetchMilestones = useCallback(async () => {
)}
+
setDrawerOpen(false)}
+ onTaskToggle={(id, newStatus) => {
+ // optimistic local patch or just refetch
+ fetchMilestones(); // simplest: keep server source of truth
+ }}
+ />
+
+
{/* 7) AI Next Steps */}
{/*
diff --git a/src/components/EditableCareerGoals.js b/src/components/EditableCareerGoals.js
new file mode 100644
index 0000000..be5e439
--- /dev/null
+++ b/src/components/EditableCareerGoals.js
@@ -0,0 +1,57 @@
+// src/components/EditableCareerGoals.js
+import React, { useState } from 'react';
+import { Button } from './ui/button.js';
+import { Pencil, Save } from 'lucide-react';
+import authFetch from '../utils/authFetch.js';
+
+export default function EditableCareerGoals({ initialGoals='', careerProfileId, onSaved }) {
+ const [editing , setEditing ] = useState(false);
+ const [draftText, setDraftText] = useState(initialGoals);
+ const [saving , setSaving ] = useState(false);
+
+ async function save() {
+ setSaving(true);
+ const res = await authFetch(`/api/premium/career-profile/${careerProfileId}/goals`, {
+ method : 'PUT',
+ headers: { 'Content-Type':'application/json' },
+ body : JSON.stringify({ career_goals: draftText })
+ });
+ if (res.ok) {
+ onSaved(draftText);
+ setEditing(false);
+ }
+ setSaving(false);
+ }
+
+ return (
+
+
+
Your Career Goals
+ {!editing && (
+
+ )}
+
+
+ {editing ? (
+ <>
+
+ );
+}
diff --git a/src/components/MilestoneDrawer.js b/src/components/MilestoneDrawer.js
new file mode 100644
index 0000000..8a5e8c0
--- /dev/null
+++ b/src/components/MilestoneDrawer.js
@@ -0,0 +1,144 @@
+import React, { useMemo, useState, useEffect } from 'react';
+import { Button } from './ui/button.js';
+import { Card, CardContent } from './ui/card.js';
+import { ChevronLeft, Check, Loader2 } from 'lucide-react';
+import { flattenTasks } from '../utils/taskHelpers.js';
+import authFetch from '../utils/authFetch.js';
+import format from 'date-fns/format';
+
+/* simple status → color map */
+const pillStyle = {
+ completed : 'bg-green-100 text-green-800',
+ in_progress : 'bg-blue-100 text-blue-800',
+ not_started : 'bg-gray-100 text-gray-700'
+};
+
+export default function MilestoneDrawer({
+ milestone, // ← pass a single milestone object
+ milestones = [], // still needed to compute progress %
+ open,
+ onClose,
+ onTaskToggle = () => {}
+}) {
+
+ /* gather tasks progress for this milestone */
+ const [tasks, setTasks] = useState(
+ milestone ? flattenTasks([milestone]) : []
+ );
+
+ // refresh local copy whenever the user selects a different milestone
+ useEffect(() => {
+ setTasks(milestone ? flattenTasks([milestone]) : []);
+ }, [milestone]);
+
+ const done = tasks.filter(t => t.status === 'completed').length;
+ const prog = tasks.length ? Math.round(100 * done / tasks.length) : 0;
+
+ if (!open || !milestone) return null;
+
+ async function toggle(t) {
+
+ const next = {
+ not_started : 'in_progress',
+ in_progress : 'completed',
+ completed : 'not_started' // undo
+};
+
+const newStatus = next[t.status] || 'not_started';
+
+
+ /* 1️⃣ optimistic local update */
+ setTasks(prev =>
+ prev.map(x =>
+ x.id === t.id ? { ...x, status: newStatus } : x
+ )
+ );
+
+ /* 2️⃣ inform parent so progress bars refresh elsewhere */
+ onTaskToggle(t.id, newStatus);
+
+ await authFetch(`/api/premium/tasks/${t.id}`, {
+ method : 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body : JSON.stringify({ status: newStatus })
+ });
+ }
+
+ const statusLabel = {
+ not_started : 'Not started',
+ in_progress : 'In progress',
+ completed : 'Completed'
+};
+
+ return (
+
+ {/* Header */}
+
+
+
+
{milestone.title}
+ {milestone.date && (
+
+ {format(new Date(milestone.date), 'PP')}
+
+ )}
+
+
+
+ {/* Body */}
+
+
+
+ {/* Progress bar */}
+
+
+ {/* Task list */}
+ {tasks.map(t => (
+
+
+
{t.title}
+ {t.due_date && (
+
+ {format(new Date(t.due_date), 'PP')}
+
+ )}
+
+
+
+
+ ))}
+
+ {!tasks.length && (
+
+ No tasks have been added to this milestone yet.
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/MilestonePanel.js b/src/components/MilestonePanel.js
index f0a5c54..7ef49b7 100644
--- a/src/components/MilestonePanel.js
+++ b/src/components/MilestonePanel.js
@@ -3,9 +3,9 @@ import { Button } from './ui/button.js';
/* MilestonePanel.jsx ---------------------------------- */
-export default function MilestonePanel({ groups, onEdit }) {
+export default function MilestonePanel({ groups, onEdit, onSelect }) {
return (
-