MilestoneTracker.js refactor
This commit is contained in:
parent
dd1d6bec88
commit
20214f9069
0
MilestoneTimeline.js
Normal file
0
MilestoneTimeline.js
Normal file
@ -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 {
|
||||
|
45
src/components/AISuggestedMilestones.js
Normal file
45
src/components/AISuggestedMilestones.js
Normal file
@ -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 (
|
||||
<div className="suggested-milestones">
|
||||
<h4>AI-Suggested Milestones</h4>
|
||||
<ul>{suggestedMilestones.map((m, i) => <li key={i}>{m.title} - {m.date}</li>)}</ul>
|
||||
<button onClick={confirmMilestones}>Confirm Milestones</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AISuggestedMilestones;
|
@ -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 (
|
||||
<div>
|
||||
{/* Career Cluster Selection */}
|
||||
<div>
|
||||
<h3>Select a Career Cluster</h3>
|
||||
<h3>Search for Career</h3>
|
||||
<Input
|
||||
value={selectedCluster}
|
||||
onChange={(e) => handleClusterSelect(e.target.value)}
|
||||
placeholder="Search for a Career Cluster"
|
||||
list="career-clusters"
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
placeholder="Start typing a career..."
|
||||
list="career-titles"
|
||||
/>
|
||||
<datalist id="career-clusters">
|
||||
{Object.keys(careerClusters).map((cluster, index) => (
|
||||
<option key={index} value={cluster} />
|
||||
<datalist id="career-titles">
|
||||
{careers.map((career, index) => (
|
||||
<option key={index} value={career} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
{/* Subdivision Selection based on Cluster */}
|
||||
{selectedCluster && (
|
||||
<div>
|
||||
<h3>Select a Subdivision</h3>
|
||||
<Input
|
||||
value={selectedSubdivision}
|
||||
onChange={(e) => handleSubdivisionSelect(e.target.value)}
|
||||
placeholder="Search for a Subdivision"
|
||||
list="subdivisions"
|
||||
/>
|
||||
<datalist id="subdivisions">
|
||||
{subdivisions.map((subdivision, index) => (
|
||||
<option key={index} value={subdivision} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Career Selection based on Subdivision */}
|
||||
{selectedSubdivision && (
|
||||
<div>
|
||||
<h3>Select a Career</h3>
|
||||
<Input
|
||||
value={careerSearch}
|
||||
onChange={handleCareerSearch}
|
||||
placeholder="Search for a Career"
|
||||
list="careers"
|
||||
/>
|
||||
<datalist id="careers">
|
||||
{careers
|
||||
.filter((career) => career.title.toLowerCase().includes(careerSearch)) // Filter careers based on search input
|
||||
.map((career, index) => (
|
||||
<option key={index} value={career.title} onClick={() => setSelectedCareer(career.title)} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Display selected career */}
|
||||
{selectedCareer && (
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<div>Selected Career: {selectedCareer}</div>
|
||||
{!hasCareerPath && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const matchedCareer = careers.find(
|
||||
(c) => c.title.toLowerCase() === selectedCareer.toLowerCase()
|
||||
);
|
||||
if (matchedCareer) {
|
||||
onSelectCareer(matchedCareer.title, matchedCareer.soc); // 🔥 this triggers the update upstream
|
||||
} else {
|
||||
alert("Please select a valid career from the list.");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Confirm Career Selection
|
||||
<button onClick={handleConfirmCareer}>
|
||||
Confirm New Career
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasCareerPath && (
|
||||
<p>This career already has a career path. Do you want to reload it or create a new one?</p>
|
||||
)}
|
||||
{isCurrentCareer && (
|
||||
<p>You are already on this career path.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
28
src/components/CareerSelectDropdown.js
Normal file
28
src/components/CareerSelectDropdown.js
Normal file
@ -0,0 +1,28 @@
|
||||
// src/components/CareerSelectDropdown.js
|
||||
import React from 'react';
|
||||
|
||||
const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading }) => {
|
||||
return (
|
||||
<div className="career-select-dropdown">
|
||||
<label>Select Career Path:</label>
|
||||
{loading ? (
|
||||
<p>Loading career paths...</p>
|
||||
) : (
|
||||
<select
|
||||
value={selectedCareer || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
|
||||
<option value="" disabled>Select career path...</option>
|
||||
{existingCareerPaths.map((path) => (
|
||||
<option key={path.career_path_id} value={path.career_name}>
|
||||
{path.career_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CareerSelectDropdown;
|
@ -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 };
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
@ -40,14 +39,67 @@ function Dashboard() {
|
||||
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,13 +361,15 @@ function Dashboard() {
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
const sessionModal = showSessionExpiredModal && (
|
||||
{showSessionExpiredModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<h3>Session Expired</h3>
|
||||
<p>Your session has expired or is invalid.</p>
|
||||
<div className="modal-actions">
|
||||
<button className="confirm-btn" onClick={() => navigate("/signin")}>Stay Signed In</button>
|
||||
<button className="confirm-btn" onClick={() => setShowSessionExpiredModal(false)}>
|
||||
Stay Signed In
|
||||
</button>
|
||||
<button className="confirm-btn" onClick={() => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("UserId");
|
||||
@ -325,7 +381,8 @@ function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
|
||||
<div className="dashboard-content">
|
||||
<div className="career-suggestions-container">
|
||||
<div
|
||||
|
127
src/components/MilestoneTimeline.js
Normal file
127
src/components/MilestoneTimeline.js
Normal file
@ -0,0 +1,127 @@
|
||||
// src/components/MilestoneTimeline.js
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
const today = new Date();
|
||||
|
||||
const MilestoneTimeline = ({ careerPathId, authFetch }) => {
|
||||
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 (
|
||||
<div className="milestone-timeline">
|
||||
<div className="view-selector">
|
||||
{['Career', 'Financial', 'Retirement'].map((view) => (
|
||||
<button key={view} className={activeView === view ? 'active' : ''} onClick={() => setActiveView(view)}>
|
||||
{view}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="timeline">
|
||||
{milestones[activeView]?.map((m) => (
|
||||
<div key={m.id} className="milestone-entry">
|
||||
<h4>{m.title}</h4>
|
||||
<p>{m.description}</p>
|
||||
<p>Date: {m.date}</p>
|
||||
<p>Progress: {m.progress}%</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button onClick={() => setShowForm(true)}>+ New Milestone</button>
|
||||
|
||||
{showForm && (
|
||||
<div className="form">
|
||||
<input type="text" placeholder="Title" value={newMilestone.title} onChange={(e) => setNewMilestone({ ...newMilestone, title: e.target.value })} />
|
||||
<input type="date" value={newMilestone.date} onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })} />
|
||||
<input type="number" placeholder="Progress (%)" value={newMilestone.progress} onChange={(e) => setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) })} />
|
||||
<button onClick={saveMilestone}>{editingMilestone ? 'Update' : 'Add'} Milestone</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="timeline-container">
|
||||
<div className="timeline-line" />
|
||||
{milestones[activeView]?.map((m) => (
|
||||
<div key={m.id} className="milestone-post" style={{ left: `${calcPosition(m.date)}%` }} onClick={() => {
|
||||
setEditingMilestone(m);
|
||||
setNewMilestone({ title: m.title, date: m.date, progress: m.progress });
|
||||
setShowForm(true);
|
||||
}}>
|
||||
<div className="milestone-dot" />
|
||||
<div className="milestone-content">
|
||||
<div className="title">{m.title}</div>
|
||||
<div className="progress-bar">
|
||||
<div className="progress" style={{ width: `${m.progress}%` }} />
|
||||
</div>
|
||||
<div className="date">{m.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MilestoneTimeline;
|
@ -2,605 +2,125 @@
|
||||
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) {
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...options.headers },
|
||||
});
|
||||
if ([401, 403].includes(res.status)) {
|
||||
setShowSessionExpiredModal(true);
|
||||
return null;
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error("Fetch error:", err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
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`);
|
||||
useEffect(() => {
|
||||
const fetchCareerPaths = async () => {
|
||||
const res = await authFetch(`${apiURL}/premium/planned-path/all`);
|
||||
if (!res) return;
|
||||
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Organize by type
|
||||
const categorized = {
|
||||
Career: [],
|
||||
Financial: [],
|
||||
Retirement: [],
|
||||
};
|
||||
// Flatten nested array
|
||||
const flatPaths = data.careerPath.flat();
|
||||
|
||||
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,
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setMilestones(categorized);
|
||||
} catch (error) {
|
||||
console.error('Error loading milestones:', error);
|
||||
}
|
||||
};
|
||||
fetchCareerPaths();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCareer) {
|
||||
fetchAISuggestedMilestones(selectedCareer?.career_name);
|
||||
}
|
||||
}, [selectedCareer]);
|
||||
|
||||
const handleAddMilestone = async () => {
|
||||
if (!careerPathId) {
|
||||
console.error('No career_path_id available for milestone.');
|
||||
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 response = await authFetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response) return;
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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],
|
||||
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);
|
||||
};
|
||||
|
||||
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 = [
|
||||
{ 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 (
|
||||
<div className="milestone-tracker">
|
||||
{showSessionExpiredModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<h3>Session Expired</h3>
|
||||
<p>Your session has expired or is invalid.</p>
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="confirm-btn"
|
||||
onClick={() => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("UserId");
|
||||
navigate("/signin");
|
||||
}}
|
||||
>
|
||||
Go to Sign In
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => navigate('/signin')}>Go to Sign In</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="header">
|
||||
<h2>Milestone Tracker</h2>
|
||||
<p>Selected Career: {selectedCareer?.career_name || 'Not Selected'}</p>
|
||||
{loading ? <p>Loading...</p> : <p>Career Path ID: {careerPathId}</p>}
|
||||
<button onClick={() => setShowForm(!showForm)}>+ New Milestone</button>
|
||||
</div>
|
||||
|
||||
{suggestedMilestones.length > 0 && (
|
||||
<div className="suggested-milestones">
|
||||
<h3>AI-Suggested Milestones</h3>
|
||||
<ul>
|
||||
{suggestedMilestones.map((milestone, idx) => (
|
||||
<li key={idx}>{milestone.title} - {milestone.date}</li>
|
||||
))}
|
||||
</ul>
|
||||
<button onClick={confirmSuggestedMilestones}>Confirm Milestones</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="view-selector">
|
||||
{['Career', 'Financial', 'Retirement'].map((view) => (
|
||||
<button
|
||||
key={view}
|
||||
className={activeView === view ? 'active' : ''}
|
||||
onClick={() => setActiveView(view)}
|
||||
>
|
||||
{view}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showForm && careerPathId ? (
|
||||
<div className="form">
|
||||
<input type="text" placeholder="Milestone Title" value={newMilestone.title} onChange={e => setNewMilestone({ ...newMilestone, title: e.target.value })} />
|
||||
<input type="date" value={newMilestone.date} onChange={e => setNewMilestone({ ...newMilestone, date: e.target.value })} />
|
||||
<label>Progress (%)</label>
|
||||
<input type="number" placeholder="Enter progress" value={newMilestone.progress} onChange={e => setNewMilestone({ ...newMilestone, progress: Number(e.target.value) })} />
|
||||
<button onClick={editingMilestone !== null ? handleEditMilestone : handleAddMilestone}>
|
||||
{editingMilestone !== null ? 'Update Milestone' : 'Add Milestone'}
|
||||
</button>
|
||||
|
||||
{activeView === 'Financial' && (
|
||||
<>
|
||||
<label>Expected Salary Increase ($)</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Enter estimated salary boost"
|
||||
value={newMilestone.salary_increase || ''}
|
||||
onChange={e =>
|
||||
setNewMilestone({ ...newMilestone, salary_increase: parseFloat(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
showForm && <p style={{ color: 'red' }}>Please select a career before adding milestones.</p>
|
||||
)}
|
||||
|
||||
<div className="timeline-container">
|
||||
<div className="timeline-line" />
|
||||
{milestones[activeView].map((milestone, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="milestone-post"
|
||||
style={{ left: `${calculatePosition(milestone.date)}%` }}
|
||||
onClick={() => {
|
||||
setEditingMilestone(milestone);
|
||||
setNewMilestone({
|
||||
title: milestone.title,
|
||||
date: milestone.date,
|
||||
progress: milestone.progress
|
||||
});
|
||||
setShowForm(true);
|
||||
}}
|
||||
>
|
||||
<div className="milestone-dot" />
|
||||
<div className="milestone-content">
|
||||
<div className="title">{milestone.title}</div>
|
||||
<div className="progress-bar">
|
||||
<div className="progress" style={{ width: `${milestone.progress}%` }} />
|
||||
</div>
|
||||
<div className="date">{milestone.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="career-search-container minimized">
|
||||
<p>Not sure about this career path? Choose a different one here.</p>
|
||||
<CareerSearch
|
||||
onSelectCareer={handleCareerSelection}
|
||||
<CareerSelectDropdown
|
||||
existingCareerPaths={existingCareerPaths}
|
||||
initialCareer={selectedCareer}
|
||||
cluster={careerCluster}
|
||||
subdivision={careerSubdivision}
|
||||
selectedCareer={selectedCareer?.career_name}
|
||||
onChange={handleCareerChange}
|
||||
loading={!existingCareerPaths.length}
|
||||
/>
|
||||
|
||||
{selectedCareer && (
|
||||
<>
|
||||
{selectedCareer !== selectedCareer?.career_name && (
|
||||
<button onClick={() => setShowModal(true)} style={{ marginTop: '10px' }}>
|
||||
Confirm Career Selection
|
||||
</button>
|
||||
)}
|
||||
{showModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<h3>Start a New Career Path?</h3>
|
||||
<p>
|
||||
You’re about to start a <strong>brand new career path</strong> for:{" "}
|
||||
<em>{selectedCareer?.career_name || selectedCareer}</em>.
|
||||
</p>
|
||||
<p>This will reset your milestone plan. Do you want to continue?</p>
|
||||
<MilestoneTimeline careerPathId={careerPathId} authFetch={authFetch} />
|
||||
|
||||
<div className="modal-actions">
|
||||
<button onClick={() => setShowModal(false)}>Cancel</button>
|
||||
<button
|
||||
className="confirm-btn"
|
||||
onClick={() => {
|
||||
handleConfirmCareerSelection();
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
Yes, Start New Path
|
||||
<AISuggestedMilestones career={selectedCareer?.career_name} careerPathId={careerPathId} authFetch={authFetch} />
|
||||
|
||||
<CareerSearch
|
||||
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
|
||||
/>
|
||||
|
||||
{pendingCareerForModal && (
|
||||
<button onClick={handleConfirmCareerSelection}>
|
||||
Confirm Career Change to {pendingCareerForModal}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -3,7 +3,10 @@
|
||||
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;
|
||||
}
|
||||
@ -19,7 +22,10 @@ export const authFetch = async (url, options = {}, onUnauthorized) => {
|
||||
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;
|
||||
}
|
||||
@ -30,4 +36,3 @@ export const authFetch = async (url, options = {}, onUnauthorized) => {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user