MilestoneTracker functionality. Added authFetch util for modal and redirect to signin.
This commit is contained in:
parent
1a97da62f0
commit
be65fca638
@ -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...
|
@ -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;
|
||||
|
@ -64,8 +64,6 @@ const CareerSearch = ({ onSelectCareer, initialCareer }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Milestone Tracker Loaded</h2>
|
||||
|
||||
{/* Career Cluster Selection */}
|
||||
<div>
|
||||
<h3>Select a Career Cluster</h3>
|
||||
|
@ -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 (
|
||||
<div className="dashboard">
|
||||
const sessionModal = showSessionExpiredModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<h3>Session Expired</h3>
|
||||
<p>Your session has expired or is invalid.</p>
|
||||
<div className="modal-actions">
|
||||
<button className="confirm-btn" onClick={() => navigate("/signin")}>Stay Signed In</button>
|
||||
<button className="confirm-btn" onClick={() => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("UserId");
|
||||
setShowSessionExpiredModal(false);
|
||||
navigate("/signin");
|
||||
}}>
|
||||
Sign In Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="dashboard-content">
|
||||
<div className="career-suggestions-container">
|
||||
<div
|
||||
|
@ -88,3 +88,67 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* ===== Modal Styles (New Addition) ===== */
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.modal p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f2f2f2;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal button:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.modal .confirm-btn {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal .confirm-btn:hover {
|
||||
background-color: #388e3c;
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
// src/components/MilestoneTracker.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import CareerSearch from './CareerSearch.js';
|
||||
import './MilestoneTracker.css';
|
||||
|
||||
const today = new Date();
|
||||
|
||||
|
||||
const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [activeView, setActiveView] = useState('Career');
|
||||
const [milestones, setMilestones] = useState({
|
||||
Career: [],
|
||||
@ -17,67 +22,203 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
|
||||
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 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 (data && data.id) {
|
||||
setCareerPathId(data.id);
|
||||
} else {
|
||||
setCareerPathId(null); // No existing career path for new user
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Could not fetch latest career path:", error);
|
||||
setCareerPathId(null);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadLastSelectedCareer = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
throw new Error('Authorization token is missing');
|
||||
}
|
||||
const authFetch = async (url, options = {}) => {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
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);
|
||||
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');
|
||||
|
||||
@ -172,20 +324,23 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
|
||||
};
|
||||
|
||||
|
||||
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,6 +352,32 @@ 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}`);
|
||||
@ -208,12 +389,41 @@ 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 = () => {
|
||||
@ -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 (
|
||||
<div className="milestone-tracker">
|
||||
{showSessionExpiredModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<h3>Session Expired</h3>
|
||||
<p>Your session has expired or is invalid.</p>
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="confirm-btn"
|
||||
onClick={() => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("UserId");
|
||||
navigate("/signin");
|
||||
}}
|
||||
>
|
||||
Go to Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="header">
|
||||
<h2>Milestone Tracker</h2>
|
||||
<p>Selected Career: {selectedCareer || 'Not Selected'}</p>
|
||||
<p>Selected Career: {selectedCareer?.career_name || 'Not Selected'}</p>
|
||||
{loading ? <p>Loading...</p> : <p>Career Path ID: {careerPathId}</p>}
|
||||
<button onClick={() => setShowForm(!showForm)}>+ New Milestone</button>
|
||||
</div>
|
||||
|
||||
@ -259,15 +498,33 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<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={handleAddMilestone}>{editingMilestone !== null ? 'Update Milestone' : 'Add Milestone'}</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" />
|
||||
@ -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);
|
||||
}}
|
||||
>
|
||||
<div className="milestone-dot" />
|
||||
<div className="milestone-content">
|
||||
@ -291,9 +556,48 @@ const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) =>
|
||||
</div>
|
||||
|
||||
<div className="career-search-container minimized">
|
||||
<p>Not sure about this career path? Choose a different one here.</p>
|
||||
<CareerSearch onSelectCareer={handleCareerSelection} initialCareer={selectedCareer} cluster={careerCluster} subdivision={careerSubdivision} />
|
||||
</div>
|
||||
<p>Not sure about this career path? Choose a different one here.</p>
|
||||
<CareerSearch
|
||||
onSelectCareer={handleCareerSelection}
|
||||
initialCareer={selectedCareer}
|
||||
cluster={careerCluster}
|
||||
subdivision={careerSubdivision}
|
||||
/>
|
||||
|
||||
{selectedCareer && !careerPathId && (
|
||||
<>
|
||||
<button onClick={() => setShowModal(true)} style={{ marginTop: '10px' }}>
|
||||
Confirm Career Selection
|
||||
</button>
|
||||
|
||||
{showModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<h3>Start a New Career Path?</h3>
|
||||
<p>
|
||||
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>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button onClick={() => setShowModal(false)}>Cancel</button>
|
||||
<button
|
||||
className="confirm-btn"
|
||||
onClick={() => {
|
||||
handleConfirmCareerSelection();
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
Yes, Start New Path
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<div className="popout-panel">
|
||||
@ -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 (
|
||||
<div className="popout-panel">
|
||||
{/* Header with Close & Plan My Path Buttons */}
|
||||
<div className="panel-header">
|
||||
<button className="close-btn" onClick={closePanel}>X</button>
|
||||
<button
|
||||
className="plan-path-btn"
|
||||
onClick={() => {
|
||||
console.log("Navigating to Milestone Tracker with career title:", title); // Log the title
|
||||
navigate("/milestone-tracker", { state: { career: title } });
|
||||
}}
|
||||
>
|
||||
Plan My Path
|
||||
</button>
|
||||
className="plan-path-btn"
|
||||
onClick={handlePlanMyPath} // 🔥 Use the already-defined, correct handler
|
||||
>
|
||||
Plan My Path
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>{title}</h2>
|
||||
|
@ -1,29 +0,0 @@
|
||||
export const fetchWithAuth = async (url, options = {}) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/signin'; // Redirect if no token
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn('Token expired or unauthorized access, redirecting to sign-in.');
|
||||
localStorage.removeItem('token'); // Clear expired token
|
||||
window.location.href = '/signin'; // Redirect user
|
||||
return;
|
||||
}
|
||||
|
||||
return await response.json(); // Return the JSON response
|
||||
} catch (error) {
|
||||
console.error('Error fetching API:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
33
src/utils/authFetch.js
Normal file
33
src/utils/authFetch.js
Normal file
@ -0,0 +1,33 @@
|
||||
// src/utils/authFetch.js
|
||||
|
||||
export const authFetch = async (url, options = {}, onUnauthorized) => {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
if (!token) {
|
||||
if (typeof onUnauthorized === 'function') onUnauthorized();
|
||||
return null;
|
||||
}
|
||||
|
||||
const finalOptions = {
|
||||
...options,
|
||||
headers: {
|
||||
...(options.headers || {}),
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(url, finalOptions);
|
||||
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
if (typeof onUnauthorized === 'function') onUnauthorized();
|
||||
return null;
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error("Fetch error:", err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user