MilestoneTracker functionality. Added authFetch util for modal and redirect to signin.

This commit is contained in:
Josh 2025-03-25 12:50:26 +00:00
parent 1a97da62f0
commit be65fca638
10 changed files with 724 additions and 187 deletions

View File

@ -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...

View File

@ -6,6 +6,7 @@ import dotenv from 'dotenv';
import { open } from 'sqlite'; import { open } from 'sqlite';
import sqlite3 from 'sqlite3'; import sqlite3 from 'sqlite3';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@ -42,7 +43,8 @@ const authenticatePremiumUser = (req, res, next) => {
if (!token) return res.status(401).json({ error: 'Premium authorization required' }); if (!token) return res.status(401).json({ error: 'Premium authorization required' });
try { 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; req.userId = userId;
next(); next();
} catch (error) { } catch (error) {
@ -80,39 +82,75 @@ app.get('/api/premium/planned-path/all', authenticatePremiumUser, async (req, re
// Save a new planned path // Save a new planned path
app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) => { 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) { if (!career_name) {
return res.status(400).json({ error: 'Job title is required' }); return res.status(400).json({ error: 'Career name is required.' });
} }
try { try {
await db.run( // Check if the career path already exists for the user
`INSERT INTO career_path (user_id, job_title, status, start_date, projected_end_date) const existingCareerPath = await db.get(
VALUES (?, ?, 'Active', DATE('now'), ?)`, `SELECT id FROM career_path WHERE user_id = ? AND career_name = ?`,
[req.userId, job_title, projected_end_date || null] [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) { } catch (error) {
console.error('Error adding planned path:', error); console.error('Error saving career path:', error);
res.status(500).json({ error: 'Failed to add planned path' }); res.status(500).json({ error: 'Failed to save career path.' });
} }
}); });
// Save a new milestone // Save a new milestone
app.post('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { 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' }); return res.status(400).json({ error: 'Missing required fields' });
} }
try { try {
await db.run( await db.run(
`INSERT INTO milestones (user_id, milestone_type, description, date, career_path_id, progress, created_at) `INSERT INTO milestones (
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`, user_id, milestone_type, title, description, date, career_path_id,
[req.userId, milestone_type, description, date, career_path_id, progress || 0] 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' }); res.status(201).json({ message: 'Milestone saved successfully' });
} catch (error) { } catch (error) {
@ -121,6 +159,8 @@ app.post('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
} }
}); });
// Get all milestones // Get all milestones
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
try { try {
@ -130,15 +170,15 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
); );
const mapped = milestones.map(m => ({ const mapped = milestones.map(m => ({
id: m.id, title: m.title,
title: m.description, description: m.description,
date: m.date, date: m.date,
type: m.milestone_type, type: m.milestone_type,
progress: m.progress || 0, progress: m.progress || 0,
career_path_id: m.career_path_id career_path_id: m.career_path_id
})); }));
res.json({ milestones: mapped }); res.json({ milestones });
} catch (error) { } catch (error) {
console.error('Error fetching milestones:', error); console.error('Error fetching milestones:', error);
res.status(500).json({ error: 'Failed to fetch milestones' }); 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 /// Update an existing milestone
app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => { 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 { 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( await db.run(
`UPDATE milestones SET milestone_type = ?, description = ?, date = ?, progress = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?`, `UPDATE milestones SET
[milestone_type, description, date, progress || 0, id, req.userId] 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' }); res.status(200).json({ message: 'Milestone updated successfully' });
} catch (error) { } 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' }); 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 // Archive current career to history
app.post('/api/premium/career-history', authenticatePremiumUser, async (req, res) => { app.post('/api/premium/career-history', authenticatePremiumUser, async (req, res) => {
const { career_path_id, company } = req.body; const { career_path_id, company } = req.body;

View File

@ -64,8 +64,6 @@ const CareerSearch = ({ onSelectCareer, initialCareer }) => {
return ( return (
<div> <div>
<h2>Milestone Tracker Loaded</h2>
{/* Career Cluster Selection */} {/* Career Cluster Selection */}
<div> <div>
<h3>Select a Career Cluster</h3> <h3>Select a Career Cluster</h3>

View File

@ -9,6 +9,7 @@ import MilestoneTracker from './MilestoneTracker.js'
import './Dashboard.css'; import './Dashboard.css';
import Chatbot from "./Chatbot.js"; import Chatbot from "./Chatbot.js";
import { Bar } from 'react-chartjs-2'; import { Bar } from 'react-chartjs-2';
import { authFetch } from '../utils/authFetch.js';
import { fetchSchools } from '../utils/apiUtils.js'; import { fetchSchools } from '../utils/apiUtils.js';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
@ -36,6 +37,16 @@ function Dashboard() {
const [selectedFit, setSelectedFit] = useState(''); const [selectedFit, setSelectedFit] = useState('');
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
const [chatbotContext, setChatbotContext] = 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 = { const jobZoneLabels = {
'1': 'Little or No Preparation', '1': 'Little or No Preparation',
@ -143,29 +154,22 @@ function Dashboard() {
}, [location.state, navigate]); }, [location.state, navigate]);
useEffect(() => { useEffect(() => {
const fetchUserProfile = async () => { const fetchUserProfile = async () => {
try { const res = await authFetch(`${apiUrl}/user-profile`, {}, handleUnauthorized);
const token = localStorage.getItem('token'); if (!res) return;
const profileResponse = await fetch(`${apiUrl}/user-profile`, {
headers: { Authorization: `Bearer ${token}` },
});
if (profileResponse.ok) { if (res.ok) {
const profileData = await profileResponse.json(); const profileData = await res.json();
setUserState(profileData.state); setUserState(profileData.state);
setAreaTitle(profileData.area.trim() || ''); setAreaTitle(profileData.area.trim() || '');
setUserZipcode(profileData.zipcode); setUserZipcode(profileData.zipcode);
} else { } else {
console.error('Failed to fetch user profile'); console.error('Failed to fetch user profile');
} }
} catch (error) { };
console.error('Error fetching user profile:', error);
}
};
fetchUserProfile();
fetchUserProfile(); }, [apiUrl]);
}, [apiUrl]);
useEffect(() => { useEffect(() => {
if ( if (
@ -303,6 +307,25 @@ function Dashboard() {
return ( return (
<div className="dashboard"> <div className="dashboard">
const sessionModal = showSessionExpiredModal && (
<div className="modal-overlay">
<div className="modal">
<h3>Session Expired</h3>
<p>Your session has expired or is invalid.</p>
<div className="modal-actions">
<button className="confirm-btn" onClick={() => navigate("/signin")}>Stay Signed In</button>
<button className="confirm-btn" onClick={() => {
localStorage.removeItem("token");
localStorage.removeItem("UserId");
setShowSessionExpiredModal(false);
navigate("/signin");
}}>
Sign In Again
</button>
</div>
</div>
</div>
);
<div className="dashboard-content"> <div className="dashboard-content">
<div className="career-suggestions-container"> <div className="career-suggestions-container">
<div <div

View File

@ -87,4 +87,68 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
} }
/* ===== Modal Styles (New Addition) ===== */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal {
background-color: white;
padding: 2rem;
border-radius: 10px;
max-width: 400px;
width: 90%;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
text-align: center;
}
.modal h3 {
margin-bottom: 0.5rem;
}
.modal p {
margin: 0.5rem 0;
font-size: 14px;
}
.modal-actions {
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
gap: 10px;
}
.modal button {
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #ccc;
background-color: #f2f2f2;
cursor: pointer;
flex: 1;
}
.modal button:hover {
background-color: #e0e0e0;
}
.modal .confirm-btn {
background-color: #4caf50;
color: white;
border: none;
}
.modal .confirm-btn:hover {
background-color: #388e3c;
}

View File

@ -1,83 +1,224 @@
// src/components/MilestoneTracker.js // src/components/MilestoneTracker.js
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
import CareerSearch from './CareerSearch.js'; import CareerSearch from './CareerSearch.js';
import './MilestoneTracker.css'; import './MilestoneTracker.css';
const today = new Date(); const today = new Date();
const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) => { const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) => {
const location = useLocation();
const navigate = useNavigate();
const [activeView, setActiveView] = useState('Career'); const [activeView, setActiveView] = useState('Career');
const [milestones, setMilestones] = useState({ const [milestones, setMilestones] = useState({
Career: [], Career: [],
Financial: [], Financial: [],
Retirement: [], Retirement: [],
}); });
const [careerPathId, setCareerPathId] = useState(null); const [careerPathId, setCareerPathId] = useState(null);
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [newMilestone, setNewMilestone] = useState({ title: '', date: '', progress: 0 }); const [newMilestone, setNewMilestone] = useState({ title: '', date: '', progress: 0 });
const [editingMilestone, setEditingMilestone] = useState(null); const [editingMilestone, setEditingMilestone] = useState(null);
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
const [selectedCareer, setSelectedCareer] = useState(initialCareer || ''); const [selectedCareer, setSelectedCareer] = useState(initialCareer || '');
const [SelectedSocCode, setSelectedSocCode] = useState('');
const [suggestedMilestones, setSuggestedMilestones] = useState([]); const [suggestedMilestones, setSuggestedMilestones] = useState([]);
const [careerCluster, setCareerCluster] = useState(''); const [careerCluster, setCareerCluster] = useState('');
const [showModal, setShowModal] = useState(false);
const [careerSubdivision, setCareerSubdivision] = useState(''); 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(() => { useEffect(() => {
loadMilestonesFromServer(); loadMilestonesFromServer();
}, [selectedCareer]); }, [selectedCareer]);
useEffect(() => { useEffect(() => {
loadLastSelectedCareer(); const token = localStorage.getItem('token');
}, []);
authFetch(`${apiURL}/premium/planned-path/latest`, {
const loadLastSelectedCareer = async () => { method: 'GET',
try { headers: { 'Content-Type': 'application/json' },
const token = localStorage.getItem('token'); })
if (!token) {
throw new Error('Authorization token is missing'); .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(); const data = await response.json();
if (data?.id) { return data;
setSelectedCareer(data.job_title); })
setCareerPathId(data.id); // Store the career_path_id
loadMilestonesFromServer(data.id); .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) => { const loadMilestonesFromServer = async (pathId = careerPathId) => {
if (!pathId) return; if (!pathId) return;
try { try {
const data = await 'https://dev1.aptivaai.com/api/premium/milestones'; const res = await authFetch(`${apiURL}/premium/milestones`);
const filtered = data.milestones.filter(m => m.career_path_id === pathId && m.milestone_type === activeView); if (!res) return;
setMilestones(prev => ({
...prev, const data = await res.json();
[activeView]: filtered.map(row => ({
id: row.id, // Organize by type
title: row.description, const categorized = {
date: row.date, Career: [],
progress: row.progress || 0, 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) { } catch (error) {
console.error('Error loading milestones:', error); console.error('Error loading milestones:', error);
} }
}; };
useEffect(() => { useEffect(() => {
if (selectedCareer) { if (selectedCareer) {
fetchAISuggestedMilestones(selectedCareer); fetchAISuggestedMilestones(selectedCareer?.career_name);
prepopulateCareerFields(selectedCareer); prepopulateCareerFields(selectedCareer?.career_name);
} }
}, [selectedCareer]); }, [selectedCareer]);
@ -104,23 +245,29 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
try { try {
const url = 'https://dev1.aptivaai.com:5002/api/premium/milestones'; const url = `${apiURL}/premium/milestones`;
const method = editingMilestone !== null ? 'PUT' : 'POST'; const method = editingMilestone !== null ? 'PUT' : 'POST';
const payload = { const payload = {
milestone_type: activeView, milestone_type: activeView,
title: newMilestone.title,
description: newMilestone.title, description: newMilestone.title,
date: newMilestone.date, date: newMilestone.date,
career_path_id: careerPathId, // Use the correct ID career_path_id: careerPathId,
progress: newMilestone.progress, 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, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
if (!response) return;
if (!response.ok) throw new Error('Error saving milestone'); if (!response.ok) throw new Error('Error saving milestone');
@ -142,23 +289,28 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
try { 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 = { const payload = {
milestone_type: activeView, milestone_type: activeView,
title: newMilestone.title, // ✅ add this
description: newMilestone.title, description: newMilestone.title,
date: newMilestone.date, date: newMilestone.date,
career_path_id: careerPathId, // Ensure milestone is linked correctly career_path_id: careerPathId,
progress: newMilestone.progress, 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', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
if (!response) return;
if (!response.ok) throw new Error('Error updating milestone'); if (!response.ok) throw new Error('Error updating milestone');
@ -170,22 +322,25 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
console.error('Error updating milestone:', error); console.error('Error updating milestone:', error);
} }
}; };
const handleCareerSelection = async (career) => { const handleCareerSelection = async (career, socCode) => {
setSelectedCareer(career); setSelectedCareer(career);
setSelectedSocCode(socCode);
prepopulateCareerFields(career); prepopulateCareerFields(career);
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
try { try {
const response = await fetch('https://dev1.aptivaai.com:5002/api/premium/planned-path', { const response = await authFetch(`${apiURL}/premium/planned-path`, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json', body: JSON.stringify({
Authorization: `Bearer ${token}`, career_path_id: uuidv4(),
}, career_name: career
body: JSON.stringify({ job_title: career }), }),
}); });
if (!response) return;
if (!response.ok) throw new Error('Error saving career path'); 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) => { const fetchAISuggestedMilestones = (career) => {
console.log(`Fetching AI suggested milestones for: ${career}`); console.log(`Fetching AI suggested milestones for: ${career}`);
const mockSuggestedMilestones = [ const mockSuggestedMilestones = [
@ -208,14 +389,43 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
setSuggestedMilestones(mockSuggestedMilestones); setSuggestedMilestones(mockSuggestedMilestones);
}; };
const confirmSuggestedMilestones = () => { const confirmSuggestedMilestones = async () => {
setMilestones((prev) => ({ if (!careerPathId) return;
...prev,
[activeView]: [...prev[activeView], ...suggestedMilestones].sort((a, b) => new Date(a.date) - new Date(b.date)), const token = localStorage.getItem('token');
}));
setSuggestedMilestones([]); 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 lastMilestoneDate = () => {
const allDates = milestones[activeView].map((m) => new Date(m.date)); const allDates = milestones[activeView].map((m) => new Date(m.date));
return allDates.length ? new Date(Math.max(...allDates)) : today; 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; 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 ( return (
<div className="milestone-tracker"> <div className="milestone-tracker">
{showSessionExpiredModal && (
<div className="modal-overlay">
<div className="modal">
<h3>Session Expired</h3>
<p>Your session has expired or is invalid.</p>
<div className="modal-actions">
<button
className="confirm-btn"
onClick={() => {
localStorage.removeItem("token");
localStorage.removeItem("UserId");
navigate("/signin");
}}
>
Go to Sign In
</button>
</div>
</div>
</div>
)}
<div className="header"> <div className="header">
<h2>Milestone Tracker</h2> <h2>Milestone Tracker</h2>
<p>Selected Career: {selectedCareer || 'Not Selected'}</p> <p>Selected Career: {selectedCareer?.career_name || 'Not Selected'}</p>
{loading ? <p>Loading...</p> : <p>Career Path ID: {careerPathId}</p>}
<button onClick={() => setShowForm(!showForm)}>+ New Milestone</button> <button onClick={() => setShowForm(!showForm)}>+ New Milestone</button>
</div> </div>
@ -259,15 +498,33 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
))} ))}
</div> </div>
{showForm && ( {showForm && careerPathId ? (
<div className="form"> <div className="form">
<input type="text" placeholder="Milestone Title" value={newMilestone.title} onChange={e => setNewMilestone({ ...newMilestone, title: e.target.value })} /> <input type="text" placeholder="Milestone Title" value={newMilestone.title} onChange={e => setNewMilestone({ ...newMilestone, title: e.target.value })} />
<input type="date" value={newMilestone.date} onChange={e => setNewMilestone({ ...newMilestone, date: e.target.value })} /> <input type="date" value={newMilestone.date} onChange={e => setNewMilestone({ ...newMilestone, date: e.target.value })} />
<label>Progress (%)</label> <label>Progress (%)</label>
<input type="number" placeholder="Enter progress" value={newMilestone.progress} onChange={e => setNewMilestone({ ...newMilestone, progress: Number(e.target.value) })} /> <input type="number" placeholder="Enter progress" value={newMilestone.progress} onChange={e => setNewMilestone({ ...newMilestone, progress: Number(e.target.value) })} />
<button onClick={handleAddMilestone}>{editingMilestone !== null ? 'Update Milestone' : 'Add Milestone'}</button> <button onClick={editingMilestone !== null ? handleEditMilestone : handleAddMilestone}>
</div> {editingMilestone !== null ? 'Update Milestone' : 'Add Milestone'}
)} </button>
{activeView === 'Financial' && (
<>
<label>Expected Salary Increase ($)</label>
<input
type="number"
placeholder="Enter estimated salary boost"
value={newMilestone.salary_increase || ''}
onChange={e =>
setNewMilestone({ ...newMilestone, salary_increase: parseFloat(e.target.value) })
}
/>
</>
)}
</div>
) : (
showForm && <p style={{ color: 'red' }}>Please select a career before adding milestones.</p>
)}
<div className="timeline-container"> <div className="timeline-container">
<div className="timeline-line" /> <div className="timeline-line" />
@ -276,7 +533,15 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
key={idx} key={idx}
className="milestone-post" className="milestone-post"
style={{ left: `${calculatePosition(milestone.date)}%` }} style={{ left: `${calculatePosition(milestone.date)}%` }}
onClick={() => handleEditMilestone(milestone)} onClick={() => {
setEditingMilestone(milestone);
setNewMilestone({
title: milestone.title,
date: milestone.date,
progress: milestone.progress
});
setShowForm(true);
}}
> >
<div className="milestone-dot" /> <div className="milestone-dot" />
<div className="milestone-content"> <div className="milestone-content">
@ -291,9 +556,48 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
</div> </div>
<div className="career-search-container minimized"> <div className="career-search-container minimized">
<p>Not sure about this career path? Choose a different one here.</p> <p>Not sure about this career path? Choose a different one here.</p>
<CareerSearch onSelectCareer={handleCareerSelection} initialCareer={selectedCareer} cluster={careerCluster} subdivision={careerSubdivision} /> <CareerSearch
</div> onSelectCareer={handleCareerSelection}
initialCareer={selectedCareer}
cluster={careerCluster}
subdivision={careerSubdivision}
/>
{selectedCareer && !careerPathId && (
<>
<button onClick={() => setShowModal(true)} style={{ marginTop: '10px' }}>
Confirm Career Selection
</button>
{showModal && (
<div className="modal-overlay">
<div className="modal">
<h3>Start a New Career Path?</h3>
<p>
Youre about to start a <strong>brand new career path</strong> for:{" "}
<em>{selectedCareer?.career_name || selectedCareer}</em>.
</p>
<p>This will reset your milestone plan. Do you want to continue?</p>
<div className="modal-actions">
<button onClick={() => setShowModal(false)}>Cancel</button>
<button
className="confirm-btn"
onClick={() => {
handleConfirmCareerSelection();
setShowModal(false);
}}
>
Yes, Start New Path
</button>
</div>
</div>
</div>
)}
</>
)}
</div>
</div> </div>
); );
}; };

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ClipLoader } from 'react-spinners'; import { ClipLoader } from 'react-spinners';
import { v4 as uuidv4 } from 'uuid';
import LoanRepayment from './LoanRepayment.js'; import LoanRepayment from './LoanRepayment.js';
import SchoolFilters from './SchoolFilters'; import SchoolFilters from './SchoolFilters';
import './PopoutPanel.css'; import './PopoutPanel.css';
@ -23,6 +24,7 @@ function PopoutPanel({
const [sortBy, setSortBy] = useState('tuition'); // Default sorting const [sortBy, setSortBy] = useState('tuition'); // Default sorting
const [maxTuition, setMaxTuition] = useState(50000); // Set default max tuition value const [maxTuition, setMaxTuition] = useState(50000); // Set default max tuition value
const [maxDistance, setMaxDistance] = useState(200); // Set default max distance value const [maxDistance, setMaxDistance] = useState(200); // Set default max distance value
const token = localStorage.getItem('token');
const navigate = useNavigate(); const navigate = useNavigate();
@ -62,6 +64,7 @@ function PopoutPanel({
if (!isVisible) return null; if (!isVisible) return null;
if (loading || loadingCalculation) { if (loading || loadingCalculation) {
return ( return (
<div className="popout-panel"> <div className="popout-panel">
@ -72,7 +75,6 @@ function PopoutPanel({
); );
} }
// Get program length for calculating tuition // Get program length for calculating tuition
const getProgramLength = (degreeType) => { const getProgramLength = (degreeType) => {
if (degreeType?.includes("Associate")) return 2; if (degreeType?.includes("Associate")) return 2;
@ -107,20 +109,90 @@ function PopoutPanel({
return 0; 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 ( return (
<div className="popout-panel"> <div className="popout-panel">
{/* Header with Close & Plan My Path Buttons */} {/* Header with Close & Plan My Path Buttons */}
<div className="panel-header"> <div className="panel-header">
<button className="close-btn" onClick={closePanel}>X</button> <button className="close-btn" onClick={closePanel}>X</button>
<button <button
className="plan-path-btn" className="plan-path-btn"
onClick={() => { onClick={handlePlanMyPath} // 🔥 Use the already-defined, correct handler
console.log("Navigating to Milestone Tracker with career title:", title); // Log the title >
navigate("/milestone-tracker", { state: { career: title } }); Plan My Path
}} </button>
>
Plan My Path
</button>
</div> </div>
<h2>{title}</h2> <h2>{title}</h2>

View File

@ -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;
}
};

33
src/utils/authFetch.js Normal file
View File

@ -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;
}
};

Binary file not shown.