Implemented backend structure and data flows for saving milestones, career paths, etc. to MilestoneTracker.js

This commit is contained in:
Josh 2025-03-21 14:23:12 +00:00
parent d294d609d0
commit 1a97da62f0
9 changed files with 718 additions and 101 deletions

View File

@ -1,83 +0,0 @@
import React, { useState } from 'react';
const AutoSuggestFields = () => {
const [cluster, setCluster] = useState('');
const [subdivision, setSubdivision] = useState('');
const [career, setCareer] = useState('');
const clusters = ['Cluster A', 'Cluster B', 'Cluster C'];
const subdivisions = {
'Cluster A': ['Subdivision A1', 'Subdivision A2'],
'Cluster B': ['Subdivision B1', 'Subdivision B2'],
'Cluster C': ['Subdivision C1', 'Subdivision C2'],
};
const careers = {
'Subdivision A1': ['Career A1-1', 'Career A1-2'],
'Subdivision B1': ['Career B1-1', 'Career B1-2'],
'Subdivision C1': ['Career C1-1', 'Career C1-2'],
};
const handleClusterChange = (e) => {
setCluster(e.target.value);
setSubdivision('');
setCareer('');
};
const handleSubdivisionChange = (e) => {
setSubdivision(e.target.value);
setCareer('');
};
return (
<div>
<label>
Cluster:
<input
type="text"
list="cluster-options"
value={cluster}
onChange={handleClusterChange}
/>
<datalist id="cluster-options">
{clusters.map((c) => (
<option key={c} value={c} />
))}
</datalist>
</label>
<label>
Subdivision:
<input
type="text"
list="subdivision-options"
value={subdivision}
onChange={handleSubdivisionChange}
disabled={!cluster}
/>
<datalist id="subdivision-options">
{(subdivisions[cluster] || []).map((s) => (
<option key={s} value={s} />
))}
</datalist>
</label>
<label>
Career:
<select
value={career}
onChange={(e) => setCareer(e.target.value)}
disabled={!subdivision}
>
<option value="">Select a career</option>
{(careers[subdivision] || []).map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</label>
</div>
);
};
export default AutoSuggestFields;

23
MilestoneTracker.js Normal file
View File

@ -0,0 +1,23 @@
// ...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...

240
backend/server3.js Normal file
View File

@ -0,0 +1,240 @@
// server3.js - Premium Services API
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
import jwt from 'jsonwebtoken';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
const app = express();
const PORT = process.env.PREMIUM_PORT || 5002;
let db;
const initDB = async () => {
try {
db = await open({
filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db',
driver: sqlite3.Database
});
console.log('Connected to user_profile.db for Premium Services.');
} catch (error) {
console.error('Error connecting to premium database:', error);
}
};
initDB();
app.use(helmet());
app.use(express.json());
const allowedOrigins = ['https://dev1.aptivaai.com'];
app.use(cors({ origin: allowedOrigins, credentials: true }));
const authenticatePremiumUser = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Premium authorization required' });
try {
const { userId } = jwt.verify(token, process.env.SECRET_KEY);
req.userId = userId;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
};
// Get latest selected planned path
app.get('/api/premium/planned-path/latest', authenticatePremiumUser, async (req, res) => {
try {
const row = await db.get(
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`,
[req.userId]
);
res.json(row || {});
} catch (error) {
console.error('Error fetching latest career path:', error);
res.status(500).json({ error: 'Failed to fetch latest planned path' });
}
});
// Get all planned paths for the user
app.get('/api/premium/planned-path/all', authenticatePremiumUser, async (req, res) => {
try {
const rows = await db.all(
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date ASC`,
[req.userId]
);
res.json({ careerPath: rows });
} catch (error) {
console.error('Error fetching career paths:', error);
res.status(500).json({ error: 'Failed to fetch planned paths' });
}
});
// Save a new planned path
app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) => {
const { job_title, projected_end_date } = req.body;
if (!job_title) {
return res.status(400).json({ error: 'Job title 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]
);
res.status(201).json({ message: 'Planned path added successfully' });
} catch (error) {
console.error('Error adding planned path:', error);
res.status(500).json({ error: 'Failed to add planned 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;
if (!milestone_type || !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]
);
res.status(201).json({ message: 'Milestone saved successfully' });
} catch (error) {
console.error('Error saving milestone:', error);
res.status(500).json({ error: 'Failed to save milestone' });
}
});
// Get all milestones
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
try {
const milestones = await db.all(
`SELECT * FROM milestones WHERE user_id = ? ORDER BY date ASC`,
[req.userId]
);
const mapped = milestones.map(m => ({
id: m.id,
title: m.description,
date: m.date,
type: m.milestone_type,
progress: m.progress || 0,
career_path_id: m.career_path_id
}));
res.json({ milestones: mapped });
} catch (error) {
console.error('Error fetching milestones:', error);
res.status(500).json({ error: 'Failed to fetch milestones' });
}
});
/// 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 {
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]
);
res.status(200).json({ message: 'Milestone updated successfully' });
} catch (error) {
console.error('Error updating milestone:', error);
res.status(500).json({ error: 'Failed to update milestone' });
}
});
// Archive current career to history
app.post('/api/premium/career-history', authenticatePremiumUser, async (req, res) => {
const { career_path_id, company } = req.body;
if (!career_path_id || !company) {
return res.status(400).json({ error: 'Career path ID and company are required' });
}
try {
const career = await db.get(`SELECT * FROM career_path WHERE id = ? AND user_id = ?`, [career_path_id, req.userId]);
if (!career) {
return res.status(404).json({ error: 'Career path not found' });
}
await db.run(
`INSERT INTO career_history (user_id, job_title, company, start_date)
VALUES (?, ?, ?, DATE('now'))`,
[req.userId, career.job_title, company]
);
await db.run(`DELETE FROM career_path WHERE id = ?`, [career_path_id]);
res.status(201).json({ message: 'Career moved to history successfully' });
} catch (error) {
console.error('Error moving career to history:', error);
res.status(500).json({ error: 'Failed to update career history' });
}
});
// Retrieve career history
app.get('/api/premium/career-history', authenticatePremiumUser, async (req, res) => {
try {
const history = await db.all(
`SELECT * FROM career_history WHERE user_id = ? ORDER BY start_date DESC;`,
[req.userId]
);
res.json({ careerHistory: history });
} catch (error) {
console.error('Error fetching career history:', error);
res.status(500).json({ error: 'Failed to fetch career history' });
}
});
// ROI Analysis (placeholder logic)
app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) => {
try {
const userCareer = await db.get(
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`,
[req.userId]
);
if (!userCareer) return res.status(404).json({ error: 'No planned path found for user' });
const roi = {
jobTitle: userCareer.job_title,
salary: userCareer.salary,
tuition: 50000,
netGain: userCareer.salary - 50000
};
res.json(roi);
} catch (error) {
console.error('Error calculating ROI:', error);
res.status(500).json({ error: 'Failed to calculate ROI' });
}
});
app.listen(PORT, () => {
console.log(`Premium server running on http://localhost:${PORT}`);
});

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { Input } from "./ui/input.js"; // Assuming Input is a basic text input component
const CareerSearch = () => {
const CareerSearch = ({ onSelectCareer, initialCareer }) => {
const [careerClusters, setCareerClusters] = useState({});
const [selectedCluster, setSelectedCluster] = useState("");
const [selectedSubdivision, setSelectedSubdivision] = useState("");
@ -22,6 +22,22 @@ const CareerSearch = () => {
fetchCareerClusters();
}, []);
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;
}
}
}
}
setSelectedCluster('');
setSelectedSubdivision('');
}, [selectedCareer, careerClusters]);
// Handle Cluster Selection
const handleClusterSelect = (cluster) => {
setSelectedCluster(cluster);

View File

@ -5,6 +5,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
import { CareerSuggestions } from './CareerSuggestions.js';
import PopoutPanel from './PopoutPanel.js';
import MilestoneTracker from './MilestoneTracker.js'
import './Dashboard.css';
import Chatbot from "./Chatbot.js";
import { Bar } from 'react-chartjs-2';
@ -144,25 +145,24 @@ function Dashboard() {
useEffect(() => {
const fetchUserProfile = async () => {
try {
const token = localStorage.getItem('token');
const profileResponse = await fetch(`${apiUrl}/user-profile`, {
headers: { Authorization: `Bearer ${token}` },
});
if (profileResponse.ok) {
const profileData = await profileResponse.json();
const { state, area, zipcode } = profileData;
setUserState(state);
setAreaTitle(area && area.trim() ? area.trim() : '');
setUserZipcode(zipcode);
} else {
console.error('Failed to fetch user profile');
}
const token = localStorage.getItem('token');
const profileResponse = await fetch(`${apiUrl}/user-profile`, {
headers: { Authorization: `Bearer ${token}` },
});
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);
console.error('Error fetching user profile:', error);
}
};
};
fetchUserProfile();
}, [apiUrl]);
@ -283,6 +283,7 @@ function Dashboard() {
} finally {
setLoading(false);
}
},
[userState, apiUrl, areaTitle, userZipcode]
);

View File

@ -0,0 +1,90 @@
/* src/components/MilestoneTracker.css */
.milestone-tracker {
width: 100%;
max-width: 900px;
margin: auto;
text-align: center;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.view-selector {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.view-selector button {
padding: 10px 15px;
border: none;
cursor: pointer;
background: #4caf50;
color: white;
border-radius: 5px;
}
.view-selector button.active {
background: #2e7d32;
}
.timeline-container {
position: relative;
width: 100%;
height: 100px;
border-top: 2px solid #ddd;
margin-top: 20px;
display: flex;
align-items: center;
position: relative;
}
.timeline-line {
position: absolute;
width: 100%;
height: 2px;
background-color: #ccc;
top: 50%;
left: 0;
}
.milestone-post {
position: absolute;
transform: translateX(-50%);
cursor: pointer;
text-align: center;
}
.milestone-dot {
width: 10px;
height: 10px;
background-color: #4caf50;
border-radius: 50%;
margin: 0 auto 5px;
}
.milestone-content {
font-size: 12px;
}
.progress-bar {
height: 5px;
background: #ddd;
width: 50px;
margin: 5px auto;
position: relative;
}
.progress {
height: 5px;
background: #4caf50;
position: absolute;
top: 0;
left: 0;
}

View File

@ -0,0 +1,301 @@
// src/components/MilestoneTracker.js
import React, { useState, useEffect } from 'react';
import CareerSearch from './CareerSearch.js';
import './MilestoneTracker.css';
const today = new Date();
const MilestoneTracker = ({ selectedCareer: initialCareer, careerClusters }) => {
const [activeView, setActiveView] = useState('Career');
const [milestones, setMilestones] = useState({
Career: [],
Financial: [],
Retirement: [],
});
const [careerPathId, setCareerPathId] = useState(null);
const [showForm, setShowForm] = useState(false);
const [newMilestone, setNewMilestone] = useState({ title: '', date: '', progress: 0 });
const [editingMilestone, setEditingMilestone] = useState(null);
const [selectedCareer, setSelectedCareer] = useState(initialCareer || '');
const [suggestedMilestones, setSuggestedMilestones] = useState([]);
const [careerCluster, setCareerCluster] = useState('');
const [careerSubdivision, setCareerSubdivision] = useState('');
useEffect(() => {
loadMilestonesFromServer();
}, [selectedCareer]);
useEffect(() => {
loadLastSelectedCareer();
}, []);
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);
}
};
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);
setMilestones(prev => ({
...prev,
[activeView]: filtered.map(row => ({
id: row.id,
title: row.description,
date: row.date,
progress: row.progress || 0,
})),
}));
} catch (error) {
console.error('Error loading milestones:', error);
}
};
useEffect(() => {
if (selectedCareer) {
fetchAISuggestedMilestones(selectedCareer);
prepopulateCareerFields(selectedCareer);
}
}, [selectedCareer]);
const prepopulateCareerFields = (career) => {
if (!careerClusters) return;
for (const cluster in careerClusters) {
for (const subdivision in careerClusters[cluster]) {
if (careerClusters[cluster][subdivision].some(job => job.title === career)) {
setCareerCluster(cluster);
setCareerSubdivision(subdivision);
return;
}
}
}
setCareerCluster('');
setCareerSubdivision('');
};
const handleAddMilestone = async () => {
if (!careerPathId) {
console.error('No career_path_id available for milestone.');
return;
}
const token = localStorage.getItem('token');
try {
const url = 'https://dev1.aptivaai.com:5002/api/premium/milestones';
const method = editingMilestone !== null ? 'PUT' : 'POST';
const payload = {
milestone_type: activeView,
description: newMilestone.title,
date: newMilestone.date,
career_path_id: careerPathId, // Use the correct ID
progress: newMilestone.progress,
};
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
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 = `https://dev1.aptivaai.com:5002/api/premium/milestones/${editingMilestone.id}`; // ✅ ID in URL
const payload = {
milestone_type: activeView,
description: newMilestone.title,
date: newMilestone.date,
career_path_id: careerPathId, // Ensure milestone is linked correctly
progress: newMilestone.progress,
};
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
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) => {
setSelectedCareer(career);
prepopulateCareerFields(career);
const token = localStorage.getItem('token');
try {
const response = await fetch('https://dev1.aptivaai.com:5002/api/premium/planned-path', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ job_title: career }),
});
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 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 = () => {
setMilestones((prev) => ({
...prev,
[activeView]: [...prev[activeView], ...suggestedMilestones].sort((a, b) => new Date(a.date) - new Date(b.date)),
}));
setSuggestedMilestones([]);
};
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;
};
return (
<div className="milestone-tracker">
<div className="header">
<h2>Milestone Tracker</h2>
<p>Selected Career: {selectedCareer || 'Not Selected'}</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 && (
<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>
)}
<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={() => handleEditMilestone(milestone)}
>
<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} initialCareer={selectedCareer} cluster={careerCluster} subdivision={careerSubdivision} />
</div>
</div>
);
};
export default MilestoneTracker;

29
src/utils/api.js Normal file
View File

@ -0,0 +1,29 @@
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;
}
};

Binary file not shown.