diff --git a/AutoSuggestFields.js b/AutoSuggestFields.js index fe076ca..e69de29 100644 --- a/AutoSuggestFields.js +++ b/AutoSuggestFields.js @@ -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 ( -
- - - - - -
- ); -}; - -export default AutoSuggestFields; diff --git a/MilestoneTracker.js b/MilestoneTracker.js new file mode 100644 index 0000000..02f107f --- /dev/null +++ b/MilestoneTracker.js @@ -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... \ No newline at end of file diff --git a/backend/server3.js b/backend/server3.js new file mode 100644 index 0000000..f58adcd --- /dev/null +++ b/backend/server3.js @@ -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}`); +}); \ No newline at end of file diff --git a/src/components/CareerSearch.js b/src/components/CareerSearch.js index 5678162..1b71cdb 100644 --- a/src/components/CareerSearch.js +++ b/src/components/CareerSearch.js @@ -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); diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index 39dc5e9..b758d91 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -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] ); diff --git a/src/components/MilestoneTracker.css b/src/components/MilestoneTracker.css new file mode 100644 index 0000000..57e63fa --- /dev/null +++ b/src/components/MilestoneTracker.css @@ -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; + } \ No newline at end of file diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js new file mode 100644 index 0000000..f23a4d7 --- /dev/null +++ b/src/components/MilestoneTracker.js @@ -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 ( +
+
+

Milestone Tracker

+

Selected Career: {selectedCareer || 'Not Selected'}

+ +
+ + {suggestedMilestones.length > 0 && ( +
+

AI-Suggested Milestones

+ + +
+ )} + +
+ {['Career', 'Financial', 'Retirement'].map((view) => ( + + ))} +
+ + {showForm && ( +
+ setNewMilestone({ ...newMilestone, title: e.target.value })} /> + setNewMilestone({ ...newMilestone, date: e.target.value })} /> + + setNewMilestone({ ...newMilestone, progress: Number(e.target.value) })} /> + +
+ )} + +
+
+ {milestones[activeView].map((milestone, idx) => ( +
handleEditMilestone(milestone)} + > +
+
+
{milestone.title}
+
+
+
+
{milestone.date}
+
+
+ ))} +
+ +
+

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

+ +
+
+ ); +}; + +export default MilestoneTracker; diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 0000000..c6947de --- /dev/null +++ b/src/utils/api.js @@ -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; + } +}; diff --git a/user_profile.db b/user_profile.db index 9bbe93a..8d23fb2 100644 Binary files a/user_profile.db and b/user_profile.db differ