diff --git a/MilestoneTimeline.js b/MilestoneTimeline.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/server3.js b/backend/server3.js index 7e7ab1e..8b70f2e 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -82,13 +82,19 @@ 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 { career_name } = req.body; + let { career_name } = req.body; if (!career_name) { return res.status(400).json({ error: 'Career name is required.' }); } try { + // Ensure that career_name is always a string + if (typeof career_name !== 'string') { + console.warn('career_name was not a string. Converting to string.'); + career_name = String(career_name); // Convert to string + } + // 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 = ?`, @@ -96,7 +102,6 @@ app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) ); 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, @@ -104,7 +109,7 @@ app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) }); } - // Only define newCareerPathId *when* creating a new path + // Define a new career path id and insert into the database const newCareerPathId = uuidv4(); await db.run( `INSERT INTO career_path (id, user_id, career_name) VALUES (?, ?, ?)`, @@ -114,7 +119,7 @@ app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) res.status(201).json({ message: 'Career path saved.', career_path_id: newCareerPathId, - action_required: 'new_created' // Action flag for newly created path + action_required: 'new_created' }); } catch (error) { console.error('Error saving career path:', error); @@ -123,6 +128,7 @@ app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) }); + // Save a new milestone app.post('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { const { diff --git a/src/components/AISuggestedMilestones.js b/src/components/AISuggestedMilestones.js new file mode 100644 index 0000000..751b15d --- /dev/null +++ b/src/components/AISuggestedMilestones.js @@ -0,0 +1,45 @@ +// src/components/AISuggestedMilestones.js +import React, { useEffect, useState } from 'react'; + +const AISuggestedMilestones = ({ career, careerPathId, authFetch }) => { + const [suggestedMilestones, setSuggestedMilestones] = useState([]); + + useEffect(() => { + if (!career) return; + setSuggestedMilestones([ + { title: `Entry-Level ${career}`, date: '2025-06-01', progress: 0 }, + { title: `Mid-Level ${career}`, date: '2027-01-01', progress: 0 }, + { title: `Senior-Level ${career}`, date: '2030-01-01', progress: 0 }, + ]); + }, [career]); + + const confirmMilestones = async () => { + for (const milestone of suggestedMilestones) { + await authFetch(`/api/premium/milestones`, { + method: 'POST', + body: JSON.stringify({ + milestone_type: 'Career', + title: milestone.title, + description: milestone.title, + date: milestone.date, + career_path_id: careerPathId, + progress: milestone.progress, + status: 'planned', + }), + }); + } + setSuggestedMilestones([]); + }; + + if (!suggestedMilestones.length) return null; + + return ( +
+

AI-Suggested Milestones

+ + +
+ ); +}; + +export default AISuggestedMilestones; diff --git a/src/components/CareerSearch.js b/src/components/CareerSearch.js index ec8eb3c..0aba3b8 100644 --- a/src/components/CareerSearch.js +++ b/src/components/CareerSearch.js @@ -1,160 +1,74 @@ -import React, { useState, useEffect } from "react"; -import { Input } from "./ui/input.js"; // Assuming Input is a basic text input component - -const CareerSearch = ({ onSelectCareer, existingCareerPaths }) => { - const [careerClusters, setCareerClusters] = useState({}); - const [selectedCluster, setSelectedCluster] = useState(""); - const [selectedSubdivision, setSelectedSubdivision] = useState(""); - const [selectedCareer, setSelectedCareer] = useState(""); - const [careerSearch, setCareerSearch] = useState(""); +import React, { useEffect, useState } from 'react'; +import { Input } from './ui/input.js'; +const CareerSearch = ({ setPendingCareerForModal }) => { + const [careers, setCareers] = useState([]); + const [searchInput, setSearchInput] = useState(''); useEffect(() => { - const fetchCareerClusters = async () => { + const fetchCareerTitles = async () => { try { const response = await fetch('/career_clusters.json'); const data = await response.json(); - setCareerClusters(data); + + const careerTitlesSet = new Set(); + + // Iterate using Object.keys at every level (no .forEach or .map) + const clusters = Object.keys(data); + for (let i = 0; i < clusters.length; i++) { + const cluster = clusters[i]; + const subdivisions = Object.keys(data[cluster]); + + for (let j = 0; j < subdivisions.length; j++) { + const subdivision = subdivisions[j]; + const careersArray = data[cluster][subdivision]; + + for (let k = 0; k < careersArray.length; k++) { + const careerObj = careersArray[k]; + if (careerObj.title) { + careerTitlesSet.add(careerObj.title); + } + } + } + } + + setCareers([...careerTitlesSet]); + } catch (error) { - console.error("Error fetching career clusters:", error); + console.error("Error fetching or processing career_clusters.json:", error); } }; - fetchCareerClusters(); + fetchCareerTitles(); }, []); - useEffect(() => { - if (selectedCareer && careerClusters) { - for (const cluster in careerClusters) { - for (const subdivision in careerClusters[cluster]) { - if (careerClusters[cluster][subdivision].some(job => job.title === selectedCareer)) { - setSelectedCluster(cluster); - setSelectedSubdivision(subdivision); - return; - } - } - } + const handleConfirmCareer = () => { + if (careers.includes(searchInput)) { + setPendingCareerForModal(searchInput); + } else { + alert("Please select a valid career from the suggestions."); } - setSelectedCluster(''); - setSelectedSubdivision(''); - }, [selectedCareer, careerClusters]); - - // Handle Cluster Selection - const handleClusterSelect = (cluster) => { - setSelectedCluster(cluster); - setSelectedSubdivision(""); // Reset subdivision on cluster change - setSelectedCareer(""); // Reset career on cluster change }; - // Handle Subdivision Selection - const handleSubdivisionSelect = (subdivision) => { - setSelectedSubdivision(subdivision); - setSelectedCareer(""); // Reset career on subdivision change - }; - - // Handle Career Selection - const handleCareerSearch = (e) => { - const query = e.target.value.toLowerCase(); - setCareerSearch(query); - }; - - // Get subdivisions based on selected cluster - const subdivisions = selectedCluster ? Object.keys(careerClusters[selectedCluster] || {}) : []; - // Get careers based on selected subdivision - const careers = selectedSubdivision ? careerClusters[selectedCluster]?.[selectedSubdivision] || [] : []; - - // Check if the selected career already has an existing career path - const hasCareerPath = existingCareerPaths.some(career => career.title === selectedCareer); - - // Check if the selected career is the current one - const isCurrentCareer = selectedCareer === selectedCareer?.career_name; - return (
- {/* Career Cluster Selection */} -
-

Select a Career Cluster

- handleClusterSelect(e.target.value)} - placeholder="Search for a Career Cluster" - list="career-clusters" - /> - - {Object.keys(careerClusters).map((cluster, index) => ( - -
+

Search for Career

+ setSearchInput(e.target.value)} + placeholder="Start typing a career..." + list="career-titles" + /> + + {careers.map((career, index) => ( + - {/* Subdivision Selection based on Cluster */} - {selectedCluster && ( -
-

Select a Subdivision

- handleSubdivisionSelect(e.target.value)} - placeholder="Search for a Subdivision" - list="subdivisions" - /> - - {subdivisions.map((subdivision, index) => ( - -
- )} - - {/* Career Selection based on Subdivision */} - {selectedSubdivision && ( -
-

Select a Career

- - - {careers - .filter((career) => career.title.toLowerCase().includes(careerSearch)) // Filter careers based on search input - .map((career, index) => ( - -
- )} - - {/* Display selected career */} - {selectedCareer && ( -
-
Selected Career: {selectedCareer}
- {!hasCareerPath && ( - - )} -
- )} - {hasCareerPath && ( -

This career already has a career path. Do you want to reload it or create a new one?

- )} - {isCurrentCareer && ( -

You are already on this career path.

- )} +
- ); }; diff --git a/src/components/CareerSelectDropdown.js b/src/components/CareerSelectDropdown.js new file mode 100644 index 0000000..b7abb6d --- /dev/null +++ b/src/components/CareerSelectDropdown.js @@ -0,0 +1,28 @@ +// src/components/CareerSelectDropdown.js +import React from 'react'; + +const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading }) => { + return ( +
+ + {loading ? ( +

Loading career paths...

+ ) : ( + + )} +
+ ); +}; + +export default CareerSelectDropdown; diff --git a/src/components/CareerSuggestions.js b/src/components/CareerSuggestions.js index e4565bc..9a97e98 100644 --- a/src/components/CareerSuggestions.js +++ b/src/components/CareerSuggestions.js @@ -41,7 +41,6 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle updateProgress(); // βœ… Update progress on success return response.data; } catch (error) { - console.warn(`⚠️ Error fetching ${url}:`, error.response?.status); updateProgress(); // βœ… Update progress even if failed return null; } @@ -61,7 +60,6 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle }).catch((error) => { updateProgress(); if (error.response?.status === 404) { - console.warn(`⚠️ Salary data missing for ${career.title} (${career.code})`); return null; } return error.response; @@ -74,12 +72,10 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle const isSalaryMissing = salaryResponse === null || salaryResponse === undefined; const isLimitedData = isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing; - if (isLimitedData) console.log(`⚠️ Setting limitedData for ${career.title} (${career.code})`); return { ...career, limitedData: isLimitedData }; } catch (error) { - console.error(`Error checking API response for ${career.title}:`, error); return { ...career, limitedData: true }; } }); diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index 55ff3f3..b00a5fe 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -9,7 +9,6 @@ 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); @@ -39,15 +38,68 @@ function Dashboard() { const [chatbotContext, setChatbotContext] = useState({}); const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false); const [sessionHandled, setSessionHandled] = useState(false); - + + const handleUnauthorized = () => { if (!sessionHandled) { setSessionHandled(true); - setShowSessionExpiredModal(true); + setShowSessionExpiredModal(true); // Show session expired modal + } + }; + // Function to handle the token check and fetch requests + const authFetch = async (url, options = {}, onUnauthorized) => { + const token = localStorage.getItem("token"); + + if (!token) { + console.log("Token is missing, triggering session expired modal."); + if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal + return null; } + const finalOptions = { + ...options, + headers: { + ...(options.headers || {}), + Authorization: `Bearer ${token}`, // Attach the token to the request + }, + }; + + try { + const res = await fetch(url, finalOptions); + + // Log the response status for debugging + console.log("Response Status:", res.status); + + if (res.status === 401 || res.status === 403) { + console.log("Session expired, triggering session expired modal."); + if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal + return null; + } + + return res; + } catch (err) { + console.error("Fetch error:", err); + if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal + return null; + } }; + // Fetch User Profile (with proper session handling) + const fetchUserProfile = async () => { + const res = await authFetch(`${apiUrl}/user-profile`); + if (!res) return; + + 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'); + } + }; + + const jobZoneLabels = { '1': 'Little or No Preparation', '2': 'Some Preparation Needed', @@ -64,6 +116,24 @@ function Dashboard() { const apiUrl = process.env.REACT_APP_API_URL || ''; + useEffect(() => { + const fetchUserProfile = async () => { + const res = await authFetch(`${apiUrl}/user-profile`); + if (!res) return; + + 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]); + useEffect(() => { const fetchJobZones = async () => { if (careerSuggestions.length === 0) return; @@ -153,23 +223,7 @@ function Dashboard() { } }, [location.state, navigate]); - useEffect(() => { - const fetchUserProfile = async () => { - const res = await authFetch(`${apiUrl}/user-profile`, {}, handleUnauthorized); - if (!res) return; - 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]); useEffect(() => { if ( @@ -307,25 +361,28 @@ function Dashboard() { return (
- const sessionModal = showSessionExpiredModal && ( -
-
-

Session Expired

-

Your session has expired or is invalid.

-
- - -
+ {showSessionExpiredModal && ( +
+
+

Session Expired

+

Your session has expired or is invalid.

+
+ +
- ); +
+ )} +
{ + const [milestones, setMilestones] = useState({ Career: [], Financial: [], Retirement: [] }); + const [activeView, setActiveView] = useState('Career'); + const [newMilestone, setNewMilestone] = useState({ title: '', date: '', progress: 0 }); + const [showForm, setShowForm] = useState(false); + const [editingMilestone, setEditingMilestone] = useState(null); + + const fetchMilestones = useCallback(async () => { + if (!careerPathId) return; + + const res = await authFetch(`api/premium/milestones`); + if (!res) return; + + const data = await res.json(); + const categorized = { Career: [], Financial: [], Retirement: [] }; + + data.milestones.forEach((m) => { + if (m.career_path_id === careerPathId && categorized[m.milestone_type]) { + categorized[m.milestone_type].push(m); + } + }); + + setMilestones(categorized); + }, [careerPathId, authFetch]); + + // βœ… useEffect simply calls the function + useEffect(() => { + fetchMilestones(); + }, [fetchMilestones]); + + const saveMilestone = async () => { + const url = editingMilestone ? `/api/premium/milestones/${editingMilestone.id}` : `/api/premium/milestones`; + const method = editingMilestone ? 'PUT' : 'POST'; + const payload = { + milestone_type: activeView, + title: newMilestone.title, + description: newMilestone.title, + date: newMilestone.date, + career_path_id: careerPathId, + progress: newMilestone.progress, + status: newMilestone.progress === 100 ? 'completed' : 'planned', + }; + + const res = await authFetch(url, { method, body: JSON.stringify(payload) }); + if (res && res.ok) { + fetchMilestones(); + setShowForm(false); + setEditingMilestone(null); + setNewMilestone({ title: '', date: '', progress: 0 }); + } + }; + + // Calculate last milestone date properly by combining all arrays + const allMilestones = [...milestones.Career, ...milestones.Financial, ...milestones.Retirement]; + const lastDate = allMilestones.reduce( + (latest, m) => (new Date(m.date) > latest ? new Date(m.date) : latest), + today + ); + + const calcPosition = (date) => { + const start = today.getTime(); + const end = lastDate.getTime(); + const position = ((new Date(date).getTime() - start) / (end - start)) * 100; + return Math.min(Math.max(position, 0), 100); + }; + + return ( +
+
+ {['Career', 'Financial', 'Retirement'].map((view) => ( + + ))} +
+ +
+ {milestones[activeView]?.map((m) => ( +
+

{m.title}

+

{m.description}

+

Date: {m.date}

+

Progress: {m.progress}%

+
+ ))} +
+ + + + {showForm && ( +
+ setNewMilestone({ ...newMilestone, title: e.target.value })} /> + setNewMilestone({ ...newMilestone, date: e.target.value })} /> + setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) })} /> + +
+ )} + +
+
+ {milestones[activeView]?.map((m) => ( +
{ + setEditingMilestone(m); + setNewMilestone({ title: m.title, date: m.date, progress: m.progress }); + setShowForm(true); + }}> +
+
+
{m.title}
+
+
+
+
{m.date}
+
+
+ ))} +
+
+ ); +}; + +export default MilestoneTimeline; diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js index f745d7c..ce947ae 100644 --- a/src/components/MilestoneTracker.js +++ b/src/components/MilestoneTracker.js @@ -2,607 +2,127 @@ import React, { useState, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; +import CareerSelectDropdown from './CareerSelectDropdown.js'; import CareerSearch from './CareerSearch.js'; +import MilestoneTimeline from './MilestoneTimeline.js'; +import AISuggestedMilestones from './AISuggestedMilestones.js'; import './MilestoneTracker.css'; -const today = new Date(); - - -const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) => { +const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const location = useLocation(); const navigate = useNavigate(); - const [activeView, setActiveView] = useState('Career'); - const [milestones, setMilestones] = useState({ - Career: [], - Financial: [], - Retirement: [], - }); - + + const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); const [careerPathId, setCareerPathId] = useState(null); - const [showForm, setShowForm] = useState(false); - const [newMilestone, setNewMilestone] = useState({ title: '', date: '', progress: 0 }); - const [editingMilestone, setEditingMilestone] = useState(null); + const [existingCareerPaths, setExistingCareerPaths] = useState([]); 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 [hasHandledCareerPath, setHasHandledCareerPath] = useState(false); - const [existingCareerPaths, setExistingCareerPaths] = useState([]); // To store existing career paths + const [pendingCareerForModal, setPendingCareerForModal] = useState(null); - - - const handleUnauthorized = () => { - if (!sessionHandled) { - setSessionHandled(true); - setShowSessionExpiredModal(true); - } - }; const apiURL = process.env.REACT_APP_API_URL; - useEffect(() => { - const fetchExistingPaths = async () => { - const response = await fetch('/api/career-paths'); // Replace with the actual API endpoint - const data = await response.json(); - setExistingCareerPaths(data); - }; - - fetchExistingPaths(); - }, []); - - useEffect(() => { - const fromState = location.state?.selectedCareer; - - if (fromState && !hasHandledCareerPath) { - setSelectedCareer(fromState); - setCareerPathId(fromState.career_path_id); - loadMilestonesFromServer(fromState.career_path_id); - handleCareerPathDecision(fromState.career_name); - setHasHandledCareerPath(true); - } - }, [location.state]); - - - useEffect(() => { - loadMilestonesFromServer(); - }, [selectedCareer]); - - useEffect(() => { - 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 data = await response.json(); - return data; - }) - - .then(data => { - if (!location.state?.selectedCareer && data && data.id) { - setCareerPathId(data.id); - setSelectedCareer({ - career_name: data.career_name, - career_path_id: data.id, - }); - } - }) - .catch((error) => { - console.error("Could not fetch latest career path:", error); - setCareerPathId(null); - }); -}, []); - const authFetch = async (url, options = {}) => { - const token = localStorage.getItem("token"); - + const token = localStorage.getItem('token'); if (!token) { setShowSessionExpiredModal(true); return null; } - - const finalOptions = { + const res = await fetch(url, { ...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); + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...options.headers }, + }); + if ([401, 403].includes(res.status)) { + setShowSessionExpiredModal(true); return null; } + return res; }; - const handleCareerPathDecision = async (careerName) => { - if (hasHandledCareerPath || !careerName || careerName === 'Not Selected') return; // βœ… already processed, do nothing - setHasHandledCareerPath(true); // βœ… prevent duplicate handling - 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); - - const fromGettingStarted = location?.state?.fromGettingStarted; - - 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); - } - if (fromGettingStarted) { - navigate(location.pathname, { replace: true, state: {} }); // clear state - } - }; - - - 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 res = await authFetch(`${apiURL}/premium/milestones`); - if (!res) return; - - - 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); - } - }; - - - useEffect(() => { - if (selectedCareer) { - fetchAISuggestedMilestones(selectedCareer?.career_name); - } - }, [selectedCareer]); - - const handleAddMilestone = async () => { - if (!careerPathId) { - console.error('No career_path_id available for milestone.'); - return; - } + const fetchCareerPaths = async () => { + const res = await authFetch(`${apiURL}/premium/planned-path/all`); + if (!res) return; - const token = localStorage.getItem('token'); - try { - 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, - 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 data = await res.json(); - const response = await authFetch(url, { - method, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - if (!response) return; + // Flatten nested array + const flatPaths = data.careerPath.flat(); - if (!response.ok) throw new Error('Error saving milestone'); - - setNewMilestone({ title: '', date: '', progress: 0 }); - setShowForm(false); - setEditingMilestone(null); - loadMilestonesFromServer(careerPathId); - } catch (error) { - console.error('Error saving milestone:', error); - } - }; + // Handle duplicates + const uniquePaths = Array.from( + new Set(flatPaths.map(cp => cp.career_name)) + ).map(name => flatPaths.find(cp => cp.career_name === name)); + + setExistingCareerPaths(uniquePaths); + + const fromPopout = location.state?.selectedCareer; + if (fromPopout) { + setSelectedCareer(fromPopout); + setCareerPathId(fromPopout.career_path_id); + } else if (!selectedCareer) { + const latest = await authFetch(`${apiURL}/premium/planned-path/latest`); + if (latest) { + const latestData = await latest.json(); + if (latestData?.id) { + setSelectedCareer(latestData); + setCareerPathId(latestData.id); + } + } + } + }; + fetchCareerPaths(); + }, []); - - const handleEditMilestone = async () => { - if (!careerPathId || !editingMilestone) { - console.error('Missing career path ID or milestone ID for update.'); - return; - } - - const token = localStorage.getItem('token'); - try { - 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, - 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 authFetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - if (!response) return; - - if (!response.ok) throw new Error('Error updating milestone'); - - setNewMilestone({ title: '', date: '', progress: 0 }); - setShowForm(false); - setEditingMilestone(null); - loadMilestonesFromServer(careerPathId); - } catch (error) { - console.error('Error updating milestone:', error); - } - }; - - - - const handleCareerSelection = async (career, socCode) => { - setSelectedCareer(career); - setSelectedSocCode(socCode); - - const token = localStorage.getItem('token'); - try { - const response = await authFetch(`${apiURL}/premium/planned-path`, { - method: 'POST', - 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'); - - const newPath = await response.json(); - setCareerPathId(newPath.id); // βœ… Update stored career path ID - loadMilestonesFromServer(newPath.id); - } catch (error) { - console.error('Error selecting career:', error); + const handleCareerChange = (careerName) => { + const match = existingCareerPaths.find(p => p.career_name === careerName); + if (match) { + setSelectedCareer(match); + setCareerPathId(match.career_path_id); } }; 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 newId = uuidv4(); + const body = { career_path_id: newId, career_name: pendingCareerForModal, start_date: new Date().toISOString().split('T')[0] }; + const res = await authFetch(`${apiURL}/premium/planned-path`, { method: 'POST', body: JSON.stringify(body) }); + if (!res || !res.ok) return; + setSelectedCareer({ career_name: pendingCareerForModal }); + setCareerPathId(newId); + setPendingCareerForModal(null); }; - - - const fetchAISuggestedMilestones = (career) => { - console.log(`Fetching AI suggested milestones for: ${career}`); - const mockSuggestedMilestones = [ - { title: `Entry-Level Certification for ${career}`, date: '2025-06-01', progress: 0 }, - { title: `Mid-Level Position in ${career}`, date: '2027-01-01', progress: 0 }, - { title: `Senior-Level Mastery in ${career}`, date: '2030-01-01', progress: 0 }, - ]; - setSuggestedMilestones(mockSuggestedMilestones); - }; - - 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; - }; - - const calculatePosition = (date) => { - const start = today.getTime(); - const end = lastMilestoneDate().getTime(); - 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?.career_name || 'Not Selected'}

- {loading ?

Loading...

:

Career Path ID: {careerPathId}

} - -
- - {suggestedMilestones.length > 0 && ( -
-

AI-Suggested Milestones

-
    - {suggestedMilestones.map((milestone, idx) => ( -
  • {milestone.title} - {milestone.date}
  • - ))} -
- +
+
+

Session Expired

+ +
)} -
- {['Career', 'Financial', 'Retirement'].map((view) => ( - - ))} -
- - {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.

-)} -
-
- {milestones[activeView].map((milestone, idx) => ( -
{ - setEditingMilestone(milestone); - setNewMilestone({ - title: milestone.title, - date: milestone.date, - progress: milestone.progress - }); - setShowForm(true); - }} - > -
-
-
{milestone.title}
-
-
-
-
{milestone.date}
-
-
- ))} -
+ -
-

Not sure about this career path? Choose a different one here.

- + - {selectedCareer && ( - <> - {selectedCareer !== selectedCareer?.career_name && ( - - )} - {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?

+ setPendingCareerForModal(careerName)} + /> -
- - -
-
-
- )} - - )} -
+ {pendingCareerForModal && ( + + )}
); }; -export default MilestoneTracker; +export default MilestoneTracker; \ No newline at end of file diff --git a/src/utils/authFetch.js b/src/utils/authFetch.js index 7232113..505b5f4 100644 --- a/src/utils/authFetch.js +++ b/src/utils/authFetch.js @@ -2,12 +2,15 @@ export const authFetch = async (url, options = {}, onUnauthorized) => { const token = localStorage.getItem("token"); - + + console.log("Token:", token); // Log token value + if (!token) { + console.log("Token is missing, triggering onUnauthorized"); if (typeof onUnauthorized === 'function') onUnauthorized(); return null; } - + const finalOptions = { ...options, headers: { @@ -15,19 +18,21 @@ export const authFetch = async (url, options = {}, onUnauthorized) => { Authorization: `Bearer ${token}`, }, }; - + try { const res = await fetch(url, finalOptions); - + + console.log("Response Status:", res.status); // Log response status + if (res.status === 401 || res.status === 403) { + console.log("Unauthorized response received, triggering onUnauthorized"); 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 ac1c9fc..326912b 100644 Binary files a/user_profile.db and b/user_profile.db differ