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