Implemented backend structure and data flows for saving milestones, career paths, etc. to MilestoneTracker.js
This commit is contained in:
parent
d294d609d0
commit
1a97da62f0
@ -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
23
MilestoneTracker.js
Normal 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
240
backend/server3.js
Normal 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}`);
|
||||
});
|
@ -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);
|
||||
|
@ -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]
|
||||
);
|
||||
|
90
src/components/MilestoneTracker.css
Normal file
90
src/components/MilestoneTracker.css
Normal 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;
|
||||
}
|
301
src/components/MilestoneTracker.js
Normal file
301
src/components/MilestoneTracker.js
Normal 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
29
src/utils/api.js
Normal 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;
|
||||
}
|
||||
};
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user