diff --git a/MilestoneTracker.js b/MilestoneTracker.js
deleted file mode 100644
index 02f107f..0000000
--- a/MilestoneTracker.js
+++ /dev/null
@@ -1,23 +0,0 @@
-// ...existing code...
-const loadLastSelectedCareer = async () => {
- try {
- const token = localStorage.getItem('token');
- if (!token) {
- throw new Error('Authorization token is missing');
- }
-
- const response = await fetch('https://dev1.aptivaai.com:5002/api/premium/planned-path/latest', {
- headers: { Authorization: `Bearer ${token}` }, // Fixed template literal syntax
- });
-
- const data = await response.json();
- if (data?.id) {
- setSelectedCareer(data.job_title);
- setCareerPathId(data.id); // Store the career_path_id
- loadMilestonesFromServer(data.id);
- }
- } catch (error) {
- console.error('Error loading last selected career:', error);
- }
-};
-// ...existing code...
\ No newline at end of file
diff --git a/backend/server3.js b/backend/server3.js
index f58adcd..7e7ab1e 100644
--- a/backend/server3.js
+++ b/backend/server3.js
@@ -6,6 +6,7 @@ import dotenv from 'dotenv';
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
import jwt from 'jsonwebtoken';
+import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import { fileURLToPath } from 'url';
@@ -42,7 +43,8 @@ const authenticatePremiumUser = (req, res, next) => {
if (!token) return res.status(401).json({ error: 'Premium authorization required' });
try {
- const { userId } = jwt.verify(token, process.env.SECRET_KEY);
+ const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey';
+ const { userId } = jwt.verify(token, SECRET_KEY);
req.userId = userId;
next();
} catch (error) {
@@ -80,39 +82,75 @@ app.get('/api/premium/planned-path/all', authenticatePremiumUser, async (req, re
// Save a new planned path
app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) => {
- const { job_title, projected_end_date } = req.body;
+ const { career_name } = req.body;
- if (!job_title) {
- return res.status(400).json({ error: 'Job title is required' });
+ if (!career_name) {
+ return res.status(400).json({ error: 'Career name is required.' });
}
try {
- await db.run(
- `INSERT INTO career_path (user_id, job_title, status, start_date, projected_end_date)
- VALUES (?, ?, 'Active', DATE('now'), ?)`,
- [req.userId, job_title, projected_end_date || null]
+ // Check if the career path already exists for the user
+ const existingCareerPath = await db.get(
+ `SELECT id FROM career_path WHERE user_id = ? AND career_name = ?`,
+ [req.userId, career_name]
);
- res.status(201).json({ message: 'Planned path added successfully' });
+ if (existingCareerPath) {
+ // Return the existing path β do NOT define or reuse newCareerPathId
+ return res.status(200).json({
+ message: 'Career path already exists. Would you like to reload it or create a new one?',
+ career_path_id: existingCareerPath.id,
+ action_required: 'reload_or_create'
+ });
+ }
+
+ // Only define newCareerPathId *when* creating a new path
+ const newCareerPathId = uuidv4();
+ await db.run(
+ `INSERT INTO career_path (id, user_id, career_name) VALUES (?, ?, ?)`,
+ [newCareerPathId, req.userId, career_name]
+ );
+
+ res.status(201).json({
+ message: 'Career path saved.',
+ career_path_id: newCareerPathId,
+ action_required: 'new_created' // Action flag for newly created path
+ });
} catch (error) {
- console.error('Error adding planned path:', error);
- res.status(500).json({ error: 'Failed to add planned path' });
+ console.error('Error saving career path:', error);
+ res.status(500).json({ error: 'Failed to save career path.' });
}
});
+
// Save a new milestone
app.post('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
- const { milestone_type, description, date, career_path_id, progress } = req.body;
+ const {
+ milestone_type,
+ title,
+ description,
+ date,
+ career_path_id,
+ salary_increase,
+ status = 'planned',
+ date_completed = null,
+ context_snapshot = null
+ } = req.body;
- if (!milestone_type || !description || !date) {
+ if (!milestone_type || !title || !description || !date) {
return res.status(400).json({ error: 'Missing required fields' });
}
try {
await db.run(
- `INSERT INTO milestones (user_id, milestone_type, description, date, career_path_id, progress, created_at)
- VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
- [req.userId, milestone_type, description, date, career_path_id, progress || 0]
+ `INSERT INTO milestones (
+ user_id, milestone_type, title, description, date, career_path_id,
+ salary_increase, status, date_completed, context_snapshot, progress, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
+ [
+ req.userId, milestone_type, title, description, date, career_path_id,
+ salary_increase || null, status, date_completed, context_snapshot
+ ]
);
res.status(201).json({ message: 'Milestone saved successfully' });
} catch (error) {
@@ -121,6 +159,8 @@ app.post('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
}
});
+
+
// Get all milestones
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
try {
@@ -130,15 +170,15 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
);
const mapped = milestones.map(m => ({
- id: m.id,
- title: m.description,
+ title: m.title,
+ description: m.description,
date: m.date,
type: m.milestone_type,
progress: m.progress || 0,
career_path_id: m.career_path_id
}));
- res.json({ milestones: mapped });
+ res.json({ milestones });
} catch (error) {
console.error('Error fetching milestones:', error);
res.status(500).json({ error: 'Failed to fetch milestones' });
@@ -147,25 +187,80 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
/// Update an existing milestone
app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
- const { id } = req.params;
- const { milestone_type, description, date, progress } = req.body;
-
- if (!milestone_type || !description || !date) {
- return res.status(400).json({ error: 'Missing required fields' });
- }
-
try {
+ const { id } = req.params;
+ const numericId = parseInt(id, 10); // π Block-defined for SQLite safety
+
+ if (isNaN(numericId)) {
+ return res.status(400).json({ error: 'Invalid milestone ID' });
+ }
+
+ const {
+ milestone_type,
+ title,
+ description,
+ date,
+ progress,
+ status,
+ date_completed,
+ salary_increase,
+ context_snapshot,
+ } = req.body;
+
+ console.log('Updating milestone with:', {
+ milestone_type,
+ title,
+ description,
+ date,
+ progress,
+ status,
+ date_completed,
+ salary_increase,
+ context_snapshot,
+ id: numericId,
+ userId: req.userId
+ });
+
await db.run(
- `UPDATE milestones SET milestone_type = ?, description = ?, date = ?, progress = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?`,
- [milestone_type, description, date, progress || 0, id, req.userId]
+ `UPDATE milestones SET
+ milestone_type = ?, title = ?, description = ?, date = ?, progress = ?,
+ status = ?, date_completed = ?, salary_increase = ?, context_snapshot = ?,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = ? AND user_id = ?`,
+ [
+ milestone_type,
+ title,
+ description,
+ date,
+ progress || 0,
+ status || 'planned',
+ date_completed,
+ salary_increase || null,
+ context_snapshot || null,
+ numericId, // π used here in the query
+ req.userId
+ ]
);
+
res.status(200).json({ message: 'Milestone updated successfully' });
} catch (error) {
- console.error('Error updating milestone:', error);
+ console.error('Error updating milestone:', error.message, error.stack);
res.status(500).json({ error: 'Failed to update milestone' });
}
});
+
+app.delete('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
+ const { id } = req.params;
+ try {
+ await db.run(`DELETE FROM milestones WHERE id = ? AND user_id = ?`, [id, req.userId]);
+ res.status(200).json({ message: 'Milestone deleted successfully' });
+ } catch (error) {
+ console.error('Error deleting milestone:', error);
+ res.status(500).json({ error: 'Failed to delete milestone' });
+ }
+});
+
// Archive current career to history
app.post('/api/premium/career-history', authenticatePremiumUser, async (req, res) => {
const { career_path_id, company } = req.body;
diff --git a/src/components/CareerSearch.js b/src/components/CareerSearch.js
index 1b71cdb..b14b715 100644
--- a/src/components/CareerSearch.js
+++ b/src/components/CareerSearch.js
@@ -64,8 +64,6 @@ const CareerSearch = ({ onSelectCareer, initialCareer }) => {
return (
-
Milestone Tracker Loaded
-
{/* Career Cluster Selection */}
Select a Career Cluster
diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js
index b758d91..55ff3f3 100644
--- a/src/components/Dashboard.js
+++ b/src/components/Dashboard.js
@@ -9,6 +9,7 @@ import MilestoneTracker from './MilestoneTracker.js'
import './Dashboard.css';
import Chatbot from "./Chatbot.js";
import { Bar } from 'react-chartjs-2';
+import { authFetch } from '../utils/authFetch.js';
import { fetchSchools } from '../utils/apiUtils.js';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
@@ -36,6 +37,16 @@ function Dashboard() {
const [selectedFit, setSelectedFit] = useState('');
const [results, setResults] = useState([]);
const [chatbotContext, setChatbotContext] = useState({});
+ const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
+ const [sessionHandled, setSessionHandled] = useState(false);
+
+ const handleUnauthorized = () => {
+ if (!sessionHandled) {
+ setSessionHandled(true);
+ setShowSessionExpiredModal(true);
+ }
+
+ };
const jobZoneLabels = {
'1': 'Little or No Preparation',
@@ -143,29 +154,22 @@ function Dashboard() {
}, [location.state, navigate]);
useEffect(() => {
- const fetchUserProfile = async () => {
- try {
- const token = localStorage.getItem('token');
- const profileResponse = await fetch(`${apiUrl}/user-profile`, {
- headers: { Authorization: `Bearer ${token}` },
- });
+ const fetchUserProfile = async () => {
+ const res = await authFetch(`${apiUrl}/user-profile`, {}, handleUnauthorized);
+ if (!res) return;
- if (profileResponse.ok) {
- const profileData = await profileResponse.json();
- setUserState(profileData.state);
- setAreaTitle(profileData.area.trim() || '');
- setUserZipcode(profileData.zipcode);
- } else {
- console.error('Failed to fetch user profile');
- }
- } catch (error) {
- console.error('Error fetching user profile:', error);
- }
- };
+ if (res.ok) {
+ const profileData = await res.json();
+ setUserState(profileData.state);
+ setAreaTitle(profileData.area.trim() || '');
+ setUserZipcode(profileData.zipcode);
+ } else {
+ console.error('Failed to fetch user profile');
+ }
+ };
-
- fetchUserProfile();
- }, [apiUrl]);
+ fetchUserProfile();
+ }, [apiUrl]);
useEffect(() => {
if (
@@ -303,6 +307,25 @@ function Dashboard() {
return (
+ const sessionModal = showSessionExpiredModal && (
+
+
+
Session Expired
+
Your session has expired or is invalid.
+
+
+
+
+
+
+ );
{
+ const location = useLocation();
+ const navigate = useNavigate();
const [activeView, setActiveView] = useState('Career');
const [milestones, setMilestones] = useState({
Career: [],
Financial: [],
Retirement: [],
});
-
+
const [careerPathId, setCareerPathId] = useState(null);
const [showForm, setShowForm] = useState(false);
const [newMilestone, setNewMilestone] = useState({ title: '', date: '', progress: 0 });
const [editingMilestone, setEditingMilestone] = useState(null);
+ const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
const [selectedCareer, setSelectedCareer] = useState(initialCareer || '');
+ const [SelectedSocCode, setSelectedSocCode] = useState('');
const [suggestedMilestones, setSuggestedMilestones] = useState([]);
const [careerCluster, setCareerCluster] = useState('');
+ const [showModal, setShowModal] = useState(false);
const [careerSubdivision, setCareerSubdivision] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [sessionHandled, setSessionHandled] = useState(false);
+
+
+ const handleUnauthorized = () => {
+ if (!sessionHandled) {
+ setSessionHandled(true);
+ setShowSessionExpiredModal(true);
+ }
+ };
+ const apiURL = process.env.REACT_APP_API_URL;
+
+ useEffect(() => {
+ if (selectedCareer) {
+ // First check when user navigates to this page
+ handleCareerPathDecision(selectedCareer);
+ }
+ }, [selectedCareer]);
+
+ useEffect(() => {
+ if (location.state?.selectedCareer) {
+ setSelectedCareer(location.state.selectedCareer);
+ setCareerPathId(location.state.selectedCareer.career_path_id);
+ loadMilestonesFromServer(location.state.selectedCareer.career_path_id);
+ } else {
+ console.warn('No career selected; prompting user to select one.');
+ setCareerPathId(null);
+ }
+ }, [location.state]);
useEffect(() => {
loadMilestonesFromServer();
}, [selectedCareer]);
useEffect(() => {
- loadLastSelectedCareer();
- }, []);
-
- const loadLastSelectedCareer = async () => {
- try {
- const token = localStorage.getItem('token');
- if (!token) {
- throw new Error('Authorization token is missing');
+ const token = localStorage.getItem('token');
+
+ authFetch(`${apiURL}/premium/planned-path/latest`, {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ .then(async (response) => {
+ if (!response) return; // session expired
+ if (!response.ok) {
+ if (response.status === 401 || response.status === 403) {
+ throw new Error('Unauthorized or token expired');
+ } else {
+ throw new Error(`Server error (${response.status})`);
+ }
}
-
- const response = await fetch('https://dev1.aptivaai.com:5002/api/premium/planned-path/latest', {
- headers: { Authorization: `Bearer ${token}` }, // Fixed template literal syntax
- });
-
const data = await response.json();
- if (data?.id) {
- setSelectedCareer(data.job_title);
- setCareerPathId(data.id); // Store the career_path_id
- loadMilestonesFromServer(data.id);
+ return data;
+ })
+
+ .then(data => {
+ if (data && data.id) {
+ setCareerPathId(data.id);
+ } else {
+ setCareerPathId(null); // No existing career path for new user
}
- } catch (error) {
- console.error('Error loading last selected career:', error);
+ })
+ .catch((error) => {
+ console.error("Could not fetch latest career path:", error);
+ setCareerPathId(null);
+ });
+ }, []);
+
+ const authFetch = async (url, options = {}) => {
+ const token = localStorage.getItem("token");
+
+ if (!token) {
+ setShowSessionExpiredModal(true);
+ return null;
}
+
+ const finalOptions = {
+ ...options,
+ headers: {
+ ...(options.headers || {}),
+ Authorization: `Bearer ${token}`,
+ },
+ };
+
+ try {
+ const res = await fetch(url, finalOptions);
+
+ if (res.status === 401 || res.status === 403) {
+ setShowSessionExpiredModal(true);
+ return null;
+ }
+
+ return res;
+ } catch (err) {
+ console.error("Fetch error:", err);
+ return null;
+ }
+ };
+
+ const handleCareerPathDecision = async (careerName) => {
+ setLoading(true);
+ const token = localStorage.getItem('token');
+ const response = await authFetch('/api/premium/planned-path', {
+ method: 'POST',
+ body: JSON.stringify({ career_name: careerName }),
+ headers: { 'Content-Type': 'application/json' },
+ });
+ if (!response) return;
+
+
+ const data = await response.json();
+ setLoading(false);
+
+ if (data.action_required === 'reload_or_create') {
+ const decision = window.confirm( `A career path for "${data.title}" already exists.\n\nClick OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`);
+
+ if (decision) {
+ reloadExistingCareerPath(data.career_path_id);
+ } else {
+ createNewCareerPath(careerName);
+ }
+ } else if (data.action_required === 'new_created') {
+ setCareerPathId(data.career_path_id);
+ }
+ };
+
+ const reloadExistingCareerPath = (careerPathId) => {
+ console.log('Reloading career path with ID:', careerPathId);
+ setCareerPathId(careerPathId);
+ };
+
+ const createNewCareerPath = async (careerName) => {
+ console.log('Creating new career path for:', careerName);
+ const response = await authFetch('/api/premium/planned-path', {
+ method: 'POST',
+ body: JSON.stringify({ career_name: careerName, career_path_id: uuidv4() }),
+ headers: { 'Content-Type': 'application/json' },
+ });
+ if (!response) return;
+
+ if (!response.ok) {
+ console.error('Error creating new career path');
+ return;
+ }
+
+ const newPath = await response.json();
+ setCareerPathId(newPath.career_path_id);
};
const loadMilestonesFromServer = async (pathId = careerPathId) => {
if (!pathId) return;
-
+
try {
- const data = await 'https://dev1.aptivaai.com/api/premium/milestones';
- const filtered = data.milestones.filter(m => m.career_path_id === pathId && m.milestone_type === activeView);
+ const res = await authFetch(`${apiURL}/premium/milestones`);
+ if (!res) return;
- setMilestones(prev => ({
- ...prev,
- [activeView]: filtered.map(row => ({
- id: row.id,
- title: row.description,
- date: row.date,
- progress: row.progress || 0,
- })),
- }));
+
+ const data = await res.json();
+
+ // Organize by type
+ const categorized = {
+ Career: [],
+ Financial: [],
+ Retirement: [],
+ };
+
+ data.milestones.forEach(m => {
+ if (m.career_path_id === pathId && categorized[m.milestone_type]) {
+ categorized[m.milestone_type].push({
+ id: m.id,
+ title: m.description,
+ date: m.date,
+ progress: m.progress || 0,
+ });
+ }
+ });
+
+ setMilestones(categorized);
} catch (error) {
- console.error('Error loading milestones:', error);
+ console.error('Error loading milestones:', error);
}
-};
+ };
+
useEffect(() => {
if (selectedCareer) {
- fetchAISuggestedMilestones(selectedCareer);
- prepopulateCareerFields(selectedCareer);
+ fetchAISuggestedMilestones(selectedCareer?.career_name);
+ prepopulateCareerFields(selectedCareer?.career_name);
+
}
}, [selectedCareer]);
@@ -104,23 +245,29 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
const token = localStorage.getItem('token');
try {
- const url = 'https://dev1.aptivaai.com:5002/api/premium/milestones';
+ const url = `${apiURL}/premium/milestones`;
const method = editingMilestone !== null ? 'PUT' : 'POST';
const payload = {
milestone_type: activeView,
+ title: newMilestone.title,
description: newMilestone.title,
date: newMilestone.date,
- career_path_id: careerPathId, // Use the correct ID
+ career_path_id: careerPathId,
progress: newMilestone.progress,
+ status: newMilestone.progress === 100 ? 'completed' : 'planned',
+ date_completed: newMilestone.progress === 100 ? newMilestone.date : null,
+ context_snapshot: JSON.stringify(selectedCareer), // pass as stringified JSON
+ salary_increase: activeView === 'Financial' ? newMilestone.salary_increase || null : null
};
- const response = await fetch(url, {
+
+ const response = await authFetch(url, {
method,
headers: {
'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
+ if (!response) return;
if (!response.ok) throw new Error('Error saving milestone');
@@ -142,23 +289,28 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
const token = localStorage.getItem('token');
try {
- const url = `https://dev1.aptivaai.com:5002/api/premium/milestones/${editingMilestone.id}`; // β
ID in URL
+ const url = `${apiURL}/premium/milestones/${editingMilestone.id}`;
const payload = {
milestone_type: activeView,
+ title: newMilestone.title, // β
add this
description: newMilestone.title,
date: newMilestone.date,
- career_path_id: careerPathId, // Ensure milestone is linked correctly
+ career_path_id: careerPathId,
progress: newMilestone.progress,
+ status: newMilestone.progress === 100 ? 'completed' : 'planned',
+ date_completed: newMilestone.progress === 100 ? newMilestone.date : null,
+ salary_increase: activeView === 'Financial' ? newMilestone.salary_increase || null : null,
+ context_snapshot: JSON.stringify(selectedCareer),
};
- const response = await fetch(url, {
+ const response = await authFetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
+ if (!response) return;
if (!response.ok) throw new Error('Error updating milestone');
@@ -170,22 +322,25 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
console.error('Error updating milestone:', error);
}
};
+
- const handleCareerSelection = async (career) => {
+ const handleCareerSelection = async (career, socCode) => {
setSelectedCareer(career);
+ setSelectedSocCode(socCode);
prepopulateCareerFields(career);
const token = localStorage.getItem('token');
try {
- const response = await fetch('https://dev1.aptivaai.com:5002/api/premium/planned-path', {
+ const response = await authFetch(`${apiURL}/premium/planned-path`, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({ job_title: career }),
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ career_path_id: uuidv4(),
+ career_name: career
+ }),
});
+ if (!response) return;
if (!response.ok) throw new Error('Error saving career path');
@@ -197,7 +352,33 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
}
};
-
+ const handleConfirmCareerSelection = async () => {
+ const token = localStorage.getItem('token');
+ const newCareerPath = {
+ career_path_id: uuidv4(),
+ career_name: selectedCareer,
+ soc_code: SelectedSocCode,
+ start_date: new Date().toISOString().split('T')[0],
+ };
+
+ try {
+ const response = await authFetch(`${apiURL}/premium/planned-path`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newCareerPath),
+ });
+ if (!response) return;
+
+ if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
+
+ setCareerPathId(newCareerPath.career_path_id);
+ loadMilestonesFromServer(newCareerPath.career_path_id);
+ } catch (error) {
+ console.error('Error confirming career selection:', error);
+ }
+ };
+
+
const fetchAISuggestedMilestones = (career) => {
console.log(`Fetching AI suggested milestones for: ${career}`);
const mockSuggestedMilestones = [
@@ -208,14 +389,43 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
setSuggestedMilestones(mockSuggestedMilestones);
};
- const confirmSuggestedMilestones = () => {
- setMilestones((prev) => ({
- ...prev,
- [activeView]: [...prev[activeView], ...suggestedMilestones].sort((a, b) => new Date(a.date) - new Date(b.date)),
- }));
- setSuggestedMilestones([]);
+ const confirmSuggestedMilestones = async () => {
+ if (!careerPathId) return;
+
+ const token = localStorage.getItem('token');
+
+ try {
+ for (const milestone of suggestedMilestones) {
+ const payload = {
+ milestone_type: activeView,
+ title: milestone.title,
+ description: milestone.title,
+ date: milestone.date,
+ career_path_id: careerPathId,
+ progress: milestone.progress || 0,
+ status: milestone.progress === 100 ? 'completed' : 'planned',
+ date_completed: milestone.progress === 100 ? milestone.date : null,
+ context_snapshot: JSON.stringify(selectedCareer),
+ salary_increase: activeView === 'Financial' ? milestone.salary_increase || null : null
+ };
+
+ const res = await authFetch(`${apiURL}/premium/milestones`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+ if (!res) return;
+ }
+
+ setSuggestedMilestones([]);
+ loadMilestonesFromServer(careerPathId);
+ } catch (error) {
+ console.error('Error confirming suggested milestones:', error);
+ }
};
-
+
const lastMilestoneDate = () => {
const allDates = milestones[activeView].map((m) => new Date(m.date));
return allDates.length ? new Date(Math.max(...allDates)) : today;
@@ -227,11 +437,40 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
return ((new Date(date).getTime() - start) / (end - start)) * 100;
};
+ if (!careerPathId) {
+ console.warn('Career path not selected yet. Prompting user to choose one.');
+ // Continue rendering UI for selecting career path
+ }
+
+
+
+
return (
+ {showSessionExpiredModal && (
+
+
+
Session Expired
+
Your session has expired or is invalid.
+
+
+
+
+
+ )}
Milestone Tracker
-
Selected Career: {selectedCareer || 'Not Selected'}
+
Selected Career: {selectedCareer?.career_name || 'Not Selected'}
+ {loading ?
Loading...
:
Career Path ID: {careerPathId}
}
@@ -259,15 +498,33 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
))}
- {showForm && (
-
- setNewMilestone({ ...newMilestone, title: e.target.value })} />
- setNewMilestone({ ...newMilestone, date: e.target.value })} />
-
- setNewMilestone({ ...newMilestone, progress: Number(e.target.value) })} />
-
-
- )}
+ {showForm && careerPathId ? (
+
+ setNewMilestone({ ...newMilestone, title: e.target.value })} />
+ setNewMilestone({ ...newMilestone, date: e.target.value })} />
+
+ setNewMilestone({ ...newMilestone, progress: Number(e.target.value) })} />
+
+
+ {activeView === 'Financial' && (
+ <>
+
+
+ setNewMilestone({ ...newMilestone, salary_increase: parseFloat(e.target.value) })
+ }
+ />
+ >
+ )}
+
+) : (
+ showForm &&
Please select a career before adding milestones.
+)}
@@ -276,7 +533,15 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
key={idx}
className="milestone-post"
style={{ left: `${calculatePosition(milestone.date)}%` }}
- onClick={() => handleEditMilestone(milestone)}
+ onClick={() => {
+ setEditingMilestone(milestone);
+ setNewMilestone({
+ title: milestone.title,
+ date: milestone.date,
+ progress: milestone.progress
+ });
+ setShowForm(true);
+ }}
>
@@ -291,9 +556,48 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
-
Not sure about this career path? Choose a different one here.
-
-
+
Not sure about this career path? Choose a different one here.
+
+
+ {selectedCareer && !careerPathId && (
+ <>
+
+
+ {showModal && (
+
+
+
Start a New Career Path?
+
+ Youβre about to start a brand new career path for:{" "}
+ {selectedCareer?.career_name || selectedCareer}.
+
+
This will reset your milestone plan. Do you want to continue?
+
+
+
+
+
+
+
+ )}
+ >
+ )}
+
);
};
diff --git a/src/components/PopoutPanel.js b/src/components/PopoutPanel.js
index d179632..a5ac562 100644
--- a/src/components/PopoutPanel.js
+++ b/src/components/PopoutPanel.js
@@ -1,6 +1,7 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { ClipLoader } from 'react-spinners';
+import { v4 as uuidv4 } from 'uuid';
import LoanRepayment from './LoanRepayment.js';
import SchoolFilters from './SchoolFilters';
import './PopoutPanel.css';
@@ -23,6 +24,7 @@ function PopoutPanel({
const [sortBy, setSortBy] = useState('tuition'); // Default sorting
const [maxTuition, setMaxTuition] = useState(50000); // Set default max tuition value
const [maxDistance, setMaxDistance] = useState(200); // Set default max distance value
+ const token = localStorage.getItem('token');
const navigate = useNavigate();
@@ -62,6 +64,7 @@ function PopoutPanel({
if (!isVisible) return null;
+
if (loading || loadingCalculation) {
return (
@@ -72,7 +75,6 @@ function PopoutPanel({
);
}
-
// Get program length for calculating tuition
const getProgramLength = (degreeType) => {
if (degreeType?.includes("Associate")) return 2;
@@ -107,20 +109,90 @@ function PopoutPanel({
return 0;
});
+ const handlePlanMyPath = async () => {
+ const token = localStorage.getItem('token');
+
+ if (!token) {
+ alert("You need to be logged in to create a career path.");
+ return;
+ }
+
+ const careerName = title; // Get the career name from the `data` object
+
+ // First, check if a career path already exists for the selected career
+ const checkResponse = await fetch('/api/premium/planned-path/latest', {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ try {
+ const allPathsResponse = await fetch(`${process.env.REACT_APP_API_URL}/premium/planned-path/all`, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!allPathsResponse.ok) throw new Error(`HTTP error ${allPathsResponse.status}`);
+
+ const { careerPath } = await allPathsResponse.json();
+ const match = careerPath.find(path => path.career_name === data.title);
+
+ if (match) {
+ const decision = window.confirm(
+ `A career path for "${data.title}" already exists.\n\nClick OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
+ );
+
+
+ if (decision) {
+ navigate('/milestone-tracker', {
+ state: { selectedCareer: { career_path_id: match.id, career_name: data.title } }
+ });
+ return;
+ }
+ }
+
+ const newCareerPath = {
+ career_path_id: uuidv4(),
+ career_name: data.title
+ };
+
+ const newResponse = await fetch(`${process.env.REACT_APP_API_URL}/premium/planned-path`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(newCareerPath),
+ });
+
+ if (!newResponse.ok) throw new Error('Failed to create new career path.');
+
+ navigate('/milestone-tracker', {
+ state: { selectedCareer: { career_path_id: newCareerPath.career_path_id, career_name: data.title } }
+ });
+
+ } catch (error) {
+ console.error('Error in Plan My Path:', error);
+ }
+ };
+
return (
{/* Header with Close & Plan My Path Buttons */}
+ className="plan-path-btn"
+ onClick={handlePlanMyPath} // π₯ Use the already-defined, correct handler
+ >
+ Plan My Path
+
+
{title}
diff --git a/src/utils/api.js b/src/utils/api.js
deleted file mode 100644
index c6947de..0000000
--- a/src/utils/api.js
+++ /dev/null
@@ -1,29 +0,0 @@
-export const fetchWithAuth = async (url, options = {}) => {
- const token = localStorage.getItem('token');
- if (!token) {
- window.location.href = '/signin'; // Redirect if no token
- return;
- }
-
- const headers = {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
- ...options.headers,
- };
-
- try {
- const response = await fetch(url, { ...options, headers });
-
- if (response.status === 401 || response.status === 403) {
- console.warn('Token expired or unauthorized access, redirecting to sign-in.');
- localStorage.removeItem('token'); // Clear expired token
- window.location.href = '/signin'; // Redirect user
- return;
- }
-
- return await response.json(); // Return the JSON response
- } catch (error) {
- console.error('Error fetching API:', error);
- throw error;
- }
-};
diff --git a/src/utils/authFetch.js b/src/utils/authFetch.js
new file mode 100644
index 0000000..7232113
--- /dev/null
+++ b/src/utils/authFetch.js
@@ -0,0 +1,33 @@
+// src/utils/authFetch.js
+
+export const authFetch = async (url, options = {}, onUnauthorized) => {
+ const token = localStorage.getItem("token");
+
+ if (!token) {
+ if (typeof onUnauthorized === 'function') onUnauthorized();
+ return null;
+ }
+
+ const finalOptions = {
+ ...options,
+ headers: {
+ ...(options.headers || {}),
+ Authorization: `Bearer ${token}`,
+ },
+ };
+
+ try {
+ const res = await fetch(url, finalOptions);
+
+ if (res.status === 401 || res.status === 403) {
+ if (typeof onUnauthorized === 'function') onUnauthorized();
+ return null;
+ }
+
+ return res;
+ } catch (err) {
+ console.error("Fetch error:", err);
+ return null;
+ }
+ };
+
diff --git a/user_profile.db b/user_profile.db
index 8d23fb2..7f48a8f 100644
Binary files a/user_profile.db and b/user_profile.db differ