From be65fca6389849892569f2ed144179a8b2388feb Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 25 Mar 2025 12:50:26 +0000 Subject: [PATCH] MilestoneTracker functionality. Added authFetch util for modal and redirect to signin. --- MilestoneTracker.js | 23 -- backend/server3.js | 153 ++++++++-- src/components/CareerSearch.js | 2 - src/components/Dashboard.js | 65 ++-- src/components/MilestoneTracker.css | 66 +++- src/components/MilestoneTracker.js | 450 +++++++++++++++++++++++----- src/components/PopoutPanel.js | 90 +++++- src/utils/api.js | 29 -- src/utils/authFetch.js | 33 ++ user_profile.db | Bin 45056 -> 53248 bytes 10 files changed, 724 insertions(+), 187 deletions(-) delete mode 100644 MilestoneTracker.js delete mode 100644 src/utils/api.js create mode 100644 src/utils/authFetch.js 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 8d23fb248ac6b44f58fa1fb87af1c71d06095fa8..7f48a8f0f8b32723efe7eb46248eb5cd82bb3854 100644 GIT binary patch literal 53248 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 PJw~=qx}8426_fu5arT{U delta 179 zcmZozz})bFX@az%7y|