Added CareerSearch to Dashboard
This commit is contained in:
parent
1cbaa7c171
commit
2dd508c38a
@ -1,71 +1,89 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Input } from './ui/input.js';
|
|
||||||
|
|
||||||
const CareerSearch = ({ setPendingCareerForModal }) => {
|
const CareerSearch = ({ onCareerSelected }) => {
|
||||||
const [careers, setCareers] = useState([]);
|
const [careerObjects, setCareerObjects] = useState([]);
|
||||||
const [searchInput, setSearchInput] = useState('');
|
const [searchInput, setSearchInput] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCareerTitles = async () => {
|
const fetchCareerData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/career_clusters.json');
|
const response = await fetch('/career_clusters.json');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const careerTitlesSet = new Set();
|
// Create a Map keyed by title, storing one object per unique title
|
||||||
|
const uniqueByTitle = new Map();
|
||||||
|
|
||||||
// Iterate using Object.keys at every level (no .forEach or .map)
|
|
||||||
const clusters = Object.keys(data);
|
const clusters = Object.keys(data);
|
||||||
for (let i = 0; i < clusters.length; i++) {
|
for (let i = 0; i < clusters.length; i++) {
|
||||||
const cluster = clusters[i];
|
const clusterKey = clusters[i];
|
||||||
const subdivisions = Object.keys(data[cluster]);
|
const subdivisions = Object.keys(data[clusterKey]);
|
||||||
|
|
||||||
for (let j = 0; j < subdivisions.length; j++) {
|
for (let j = 0; j < subdivisions.length; j++) {
|
||||||
const subdivision = subdivisions[j];
|
const subKey = subdivisions[j];
|
||||||
const careersArray = data[cluster][subdivision];
|
const careersList = data[clusterKey][subKey] || [];
|
||||||
|
|
||||||
for (let k = 0; k < careersArray.length; k++) {
|
for (let k = 0; k < careersList.length; k++) {
|
||||||
const careerObj = careersArray[k];
|
const c = careersList[k];
|
||||||
if (careerObj.title) {
|
// If there's a title and soc_code, store the first we encounter for that title.
|
||||||
careerTitlesSet.add(careerObj.title);
|
if (c.title && c.soc_code && c.cip_code !== undefined) {
|
||||||
|
if (!uniqueByTitle.has(c.title)) {
|
||||||
|
// Add it if we haven't seen this exact title yet
|
||||||
|
uniqueByTitle.set(c.title, {
|
||||||
|
title: c.title,
|
||||||
|
soc_code: c.soc_code,
|
||||||
|
cip_code: c.cip_code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If you truly only want to keep the first occurrence,
|
||||||
|
// just do nothing if we see the same title again.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCareers([...careerTitlesSet]);
|
// Convert Map to array
|
||||||
|
const dedupedArr = [...uniqueByTitle.values()];
|
||||||
|
setCareerObjects(dedupedArr);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching or processing career_clusters.json:", error);
|
console.error('Error loading or parsing career_clusters.json:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchCareerTitles();
|
fetchCareerData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Called when user clicks "Confirm New Career"
|
||||||
const handleConfirmCareer = () => {
|
const handleConfirmCareer = () => {
|
||||||
if (careers.includes(searchInput)) {
|
// Find the full object by exact title match
|
||||||
setPendingCareerForModal(searchInput);
|
const foundObj = careerObjects.find(
|
||||||
|
(obj) => obj.title.toLowerCase() === searchInput.toLowerCase()
|
||||||
|
);
|
||||||
|
console.log('[CareerSearch] foundObj:', foundObj);
|
||||||
|
|
||||||
|
if (foundObj) {
|
||||||
|
onCareerSelected(foundObj);
|
||||||
} else {
|
} else {
|
||||||
alert("Please select a valid career from the suggestions.");
|
alert('Please select a valid career from the suggestions.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
<h3>Search for Career</h3>
|
<h3>Search for Career</h3>
|
||||||
<Input
|
<input
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={(e) => setSearchInput(e.target.value)}
|
onChange={(e) => setSearchInput(e.target.value)}
|
||||||
placeholder="Start typing a career..."
|
placeholder="Start typing a career..."
|
||||||
list="career-titles"
|
list="career-titles"
|
||||||
/>
|
/>
|
||||||
<datalist id="career-titles">
|
<datalist id="career-titles">
|
||||||
{careers.map((career, index) => (
|
{careerObjects.map((obj, index) => (
|
||||||
<option key={index} value={career} />
|
<option key={index} value={obj.title} />
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
|
|
||||||
<button onClick={handleConfirmCareer}>
|
<button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
|
||||||
Confirm New Career
|
Confirm New Career
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
// Dashboard.js
|
// Dashboard.js
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
||||||
|
|
||||||
import { CareerSuggestions } from './CareerSuggestions.js';
|
import { CareerSuggestions } from './CareerSuggestions.js';
|
||||||
import PopoutPanel from './PopoutPanel.js';
|
import PopoutPanel from './PopoutPanel.js';
|
||||||
import MilestoneTracker from './MilestoneTracker.js'
|
import MilestoneTracker from './MilestoneTracker.js';
|
||||||
import './Dashboard.css';
|
import CareerSearch from './CareerSearch.js'; // <--- Import your new search
|
||||||
import Chatbot from "./Chatbot.js";
|
import Chatbot from "./Chatbot.js";
|
||||||
|
|
||||||
|
import './Dashboard.css';
|
||||||
import { Bar } from 'react-chartjs-2';
|
import { Bar } from 'react-chartjs-2';
|
||||||
import { fetchSchools } from '../utils/apiUtils.js';
|
import { fetchSchools } from '../utils/apiUtils.js';
|
||||||
|
|
||||||
@ -17,6 +21,7 @@ function Dashboard() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// ============= Existing States =============
|
||||||
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
||||||
const [careerDetails, setCareerDetails] = useState(null);
|
const [careerDetails, setCareerDetails] = useState(null);
|
||||||
const [riaSecScores, setRiaSecScores] = useState([]);
|
const [riaSecScores, setRiaSecScores] = useState([]);
|
||||||
@ -25,8 +30,11 @@ function Dashboard() {
|
|||||||
const [salaryData, setSalaryData] = useState([]);
|
const [salaryData, setSalaryData] = useState([]);
|
||||||
const [economicProjections, setEconomicProjections] = useState(null);
|
const [economicProjections, setEconomicProjections] = useState(null);
|
||||||
const [tuitionData, setTuitionData] = useState(null);
|
const [tuitionData, setTuitionData] = useState(null);
|
||||||
|
|
||||||
|
// Overall Dashboard loading
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [userState, setUserState] = useState(null);
|
const [userState, setUserState] = useState(null);
|
||||||
const [areaTitle, setAreaTitle] = useState(null);
|
const [areaTitle, setAreaTitle] = useState(null);
|
||||||
@ -37,51 +45,55 @@ function Dashboard() {
|
|||||||
const [selectedFit, setSelectedFit] = useState('');
|
const [selectedFit, setSelectedFit] = useState('');
|
||||||
const [results, setResults] = useState([]);
|
const [results, setResults] = useState([]);
|
||||||
const [chatbotContext, setChatbotContext] = useState({});
|
const [chatbotContext, setChatbotContext] = useState({});
|
||||||
|
|
||||||
|
// Show session expired modal
|
||||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
||||||
const [sessionHandled, setSessionHandled] = useState(false);
|
const [sessionHandled, setSessionHandled] = useState(false);
|
||||||
|
|
||||||
|
// ============= NEW State =============
|
||||||
|
// Holds the full career object { title, soc_code, cip_code } typed in CareerSearch
|
||||||
|
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||||
|
|
||||||
|
// We'll treat "loading" as "loadingSuggestions"
|
||||||
const loadingSuggestions = loading;
|
const loadingSuggestions = loading;
|
||||||
|
// We'll consider the popout panel visible if there's a selectedCareer
|
||||||
const popoutVisible = !!selectedCareer;
|
const popoutVisible = !!selectedCareer;
|
||||||
|
|
||||||
// Function to handle the token check and fetch requests
|
// ============= Auth & URL Setup =============
|
||||||
const authFetch = async (url, options = {}, onUnauthorized) => {
|
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
|
// AUTH fetch
|
||||||
|
const authFetch = async (url, options = {}, onUnauthorized) => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.log("Token is missing, triggering session expired modal.");
|
console.log("Token is missing, triggering session expired modal.");
|
||||||
if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal
|
if (typeof onUnauthorized === 'function') onUnauthorized();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalOptions = {
|
const finalOptions = {
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...(options.headers || {}),
|
...(options.headers || {}),
|
||||||
Authorization: `Bearer ${token}`, // Attach the token to the request
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, finalOptions);
|
const res = await fetch(url, finalOptions);
|
||||||
|
|
||||||
// Log the response status for debugging
|
|
||||||
console.log("Response Status:", res.status);
|
console.log("Response Status:", res.status);
|
||||||
|
|
||||||
if (res.status === 401 || res.status === 403) {
|
if (res.status === 401 || res.status === 403) {
|
||||||
console.log("Session expired, triggering session expired modal.");
|
console.log("Session expired, triggering session expired modal.");
|
||||||
if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal
|
if (typeof onUnauthorized === 'function') onUnauthorized();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Fetch error:", err);
|
console.error("Fetch error:", err);
|
||||||
if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal
|
if (typeof onUnauthorized === 'function') onUnauthorized();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch User Profile (with proper session handling)
|
// ============= User Profile Fetch =============
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
const res = await authFetch(`${apiUrl}/user-profile`);
|
const res = await authFetch(`${apiUrl}/user-profile`);
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
@ -95,8 +107,13 @@ function Dashboard() {
|
|||||||
console.error('Failed to fetch user profile');
|
console.error('Failed to fetch user profile');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ============= Lifecycle: Load Profile, Setup =============
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUserProfile();
|
||||||
|
}, [apiUrl]); // load once
|
||||||
|
|
||||||
|
// ============= jobZone & fit Setup =============
|
||||||
const jobZoneLabels = {
|
const jobZoneLabels = {
|
||||||
'1': 'Little or No Preparation',
|
'1': 'Little or No Preparation',
|
||||||
'2': 'Some Preparation Needed',
|
'2': 'Some Preparation Needed',
|
||||||
@ -111,49 +128,27 @@ function Dashboard() {
|
|||||||
'Good': 'Good - Less Strong Match'
|
'Good': 'Good - Less Strong Match'
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
// Fetch job zones for each career suggestion
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUserProfile = async () => {
|
|
||||||
const res = await authFetch(`${apiUrl}/user-profile`);
|
|
||||||
if (!res) return;
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const profileData = await res.json();
|
|
||||||
setUserState(profileData.state);
|
|
||||||
setAreaTitle(profileData.area.trim() || '');
|
|
||||||
setUserZipcode(profileData.zipcode);
|
|
||||||
} else {
|
|
||||||
console.error('Failed to fetch user profile');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUserProfile();
|
|
||||||
}, [apiUrl]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchJobZones = async () => {
|
const fetchJobZones = async () => {
|
||||||
if (careerSuggestions.length === 0) return;
|
if (careerSuggestions.length === 0) return;
|
||||||
|
|
||||||
const socCodes = careerSuggestions.map((career) => career.code);
|
const socCodes = careerSuggestions.map((career) => career.code);
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
|
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
|
||||||
const jobZoneData = response.data;
|
const jobZoneData = response.data;
|
||||||
|
|
||||||
const updatedCareers = careerSuggestions.map((career) => ({
|
const updatedCareers = careerSuggestions.map((career) => ({
|
||||||
...career,
|
...career,
|
||||||
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setCareersWithJobZone(updatedCareers);
|
setCareersWithJobZone(updatedCareers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching job zone information:', error);
|
console.error('Error fetching job zone information:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchJobZones();
|
fetchJobZones();
|
||||||
}, [careerSuggestions, apiUrl]);
|
}, [careerSuggestions, apiUrl]);
|
||||||
|
|
||||||
|
// Filter careers by job zone, fit
|
||||||
const filteredCareers = useMemo(() => {
|
const filteredCareers = useMemo(() => {
|
||||||
return careersWithJobZone.filter((career) => {
|
return careersWithJobZone.filter((career) => {
|
||||||
const jobZoneMatches = selectedJobZone
|
const jobZoneMatches = selectedJobZone
|
||||||
@ -164,11 +159,11 @@ function Dashboard() {
|
|||||||
: true;
|
: true;
|
||||||
|
|
||||||
const fitMatches = selectedFit ? career.fit === selectedFit : true;
|
const fitMatches = selectedFit ? career.fit === selectedFit : true;
|
||||||
|
|
||||||
return jobZoneMatches && fitMatches;
|
return jobZoneMatches && fitMatches;
|
||||||
});
|
});
|
||||||
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
||||||
|
|
||||||
|
// Merge updated data into chatbot context
|
||||||
const updateChatbotContext = (updatedData) => {
|
const updateChatbotContext = (updatedData) => {
|
||||||
setChatbotContext((prevContext) => {
|
setChatbotContext((prevContext) => {
|
||||||
const mergedContext = {
|
const mergedContext = {
|
||||||
@ -184,10 +179,11 @@ function Dashboard() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Our final array for CareerSuggestions
|
||||||
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
|
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
|
||||||
|
|
||||||
|
// ============= Popout Panel Setup =============
|
||||||
const memoizedPopoutPanel = useMemo(() => {
|
const memoizedPopoutPanel = useMemo(() => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoutPanel
|
<PopoutPanel
|
||||||
isVisible={!!selectedCareer}
|
isVisible={!!selectedCareer}
|
||||||
@ -204,13 +200,25 @@ function Dashboard() {
|
|||||||
updateChatbotContext={updateChatbotContext}
|
updateChatbotContext={updateChatbotContext}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]);
|
}, [
|
||||||
|
selectedCareer,
|
||||||
|
careerDetails,
|
||||||
|
schools,
|
||||||
|
salaryData,
|
||||||
|
economicProjections,
|
||||||
|
tuitionData,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
userState,
|
||||||
|
results
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ============= On Page Load: get careerSuggestions from location.state, etc. =============
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let descriptions = [];
|
let descriptions = [];
|
||||||
if (location.state) {
|
if (location.state) {
|
||||||
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
|
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
|
||||||
descriptions = scores.map((score) => score.description || "No description available.");
|
descriptions = (scores || []).map((score) => score.description || "No description available.");
|
||||||
setCareerSuggestions(suggestions || []);
|
setCareerSuggestions(suggestions || []);
|
||||||
setRiaSecScores(scores || []);
|
setRiaSecScores(scores || []);
|
||||||
setRiaSecDescriptions(descriptions);
|
setRiaSecDescriptions(descriptions);
|
||||||
@ -220,8 +228,7 @@ function Dashboard() {
|
|||||||
}
|
}
|
||||||
}, [location.state, navigate]);
|
}, [location.state, navigate]);
|
||||||
|
|
||||||
|
// Once userState, areaTitle, userZipcode, etc. are set, update chatbot
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
careerSuggestions.length > 0 &&
|
careerSuggestions.length > 0 &&
|
||||||
@ -230,7 +237,6 @@ function Dashboard() {
|
|||||||
areaTitle !== null &&
|
areaTitle !== null &&
|
||||||
userZipcode !== null
|
userZipcode !== null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const newChatbotContext = {
|
const newChatbotContext = {
|
||||||
careerSuggestions: [...careersWithJobZone],
|
careerSuggestions: [...careersWithJobZone],
|
||||||
riaSecScores: [...riaSecScores],
|
riaSecScores: [...riaSecScores],
|
||||||
@ -238,15 +244,16 @@ function Dashboard() {
|
|||||||
areaTitle: areaTitle || "",
|
areaTitle: areaTitle || "",
|
||||||
userZipcode: userZipcode || "",
|
userZipcode: userZipcode || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
setChatbotContext(newChatbotContext);
|
setChatbotContext(newChatbotContext);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
|
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
|
||||||
|
|
||||||
|
// ============= handleCareerClick (for tile clicks) =============
|
||||||
const handleCareerClick = useCallback(
|
const handleCareerClick = useCallback(
|
||||||
async (career) => {
|
async (career) => {
|
||||||
|
console.log('[handleCareerClick] career =>', career);
|
||||||
const socCode = career.code;
|
const socCode = career.code;
|
||||||
|
console.log('[handleCareerClick] career.code =>', socCode);
|
||||||
setSelectedCareer(career);
|
setSelectedCareer(career);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@ -263,15 +270,18 @@ function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// CIP fetch
|
||||||
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||||
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
||||||
const { cipCode } = await cipResponse.json();
|
const { cipCode } = await cipResponse.json();
|
||||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||||
|
|
||||||
|
// Job details
|
||||||
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||||
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
||||||
const { description, tasks } = await jobDetailsResponse.json();
|
const { description, tasks } = await jobDetailsResponse.json();
|
||||||
|
|
||||||
|
// Salary
|
||||||
let salaryResponse;
|
let salaryResponse;
|
||||||
try {
|
try {
|
||||||
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } });
|
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } });
|
||||||
@ -279,6 +289,7 @@ function Dashboard() {
|
|||||||
salaryResponse = { data: {} };
|
salaryResponse = { data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Economic
|
||||||
let economicResponse;
|
let economicResponse;
|
||||||
try {
|
try {
|
||||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
|
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
|
||||||
@ -286,6 +297,7 @@ function Dashboard() {
|
|||||||
economicResponse = { data: {} };
|
economicResponse = { data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tuition
|
||||||
let tuitionResponse;
|
let tuitionResponse;
|
||||||
try {
|
try {
|
||||||
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } });
|
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } });
|
||||||
@ -293,8 +305,8 @@ function Dashboard() {
|
|||||||
tuitionResponse = { data: {} };
|
tuitionResponse = { data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch schools
|
||||||
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
||||||
|
|
||||||
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
||||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||||
try {
|
try {
|
||||||
@ -309,24 +321,47 @@ function Dashboard() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const salaryDataPoints = salaryResponse.data && Object.keys(salaryResponse.data).length > 0
|
// Build salary array
|
||||||
|
const sData = salaryResponse.data || {};
|
||||||
|
const salaryDataPoints = sData && Object.keys(sData).length > 0
|
||||||
? [
|
? [
|
||||||
{ percentile: "10th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT10, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT10, 10) || 0 },
|
{
|
||||||
{ percentile: "25th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT25, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT25, 10) || 0 },
|
percentile: "10th Percentile",
|
||||||
{ percentile: "Median", regionalSalary: parseInt(salaryResponse.data.regional?.regional_MEDIAN, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_MEDIAN, 10) || 0 },
|
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
|
||||||
{ percentile: "75th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT75, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT75, 10) || 0 },
|
nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0
|
||||||
{ percentile: "90th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT90, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT90, 10) || 0 },
|
},
|
||||||
|
{
|
||||||
|
percentile: "25th Percentile",
|
||||||
|
regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0,
|
||||||
|
nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
percentile: "Median",
|
||||||
|
regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0,
|
||||||
|
nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
percentile: "75th Percentile",
|
||||||
|
regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0,
|
||||||
|
nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
percentile: "90th Percentile",
|
||||||
|
regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0,
|
||||||
|
nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// Build final details
|
||||||
const updatedCareerDetails = {
|
const updatedCareerDetails = {
|
||||||
...career,
|
...career,
|
||||||
jobDescription: description,
|
jobDescription: description,
|
||||||
tasks: tasks,
|
tasks: tasks,
|
||||||
economicProjections: economicResponse.data || {},
|
economicProjections: (economicResponse.data || {}),
|
||||||
salaryData: salaryDataPoints,
|
salaryData: salaryDataPoints,
|
||||||
schools: schoolsWithDistance,
|
schools: schoolsWithDistance,
|
||||||
tuitionData: tuitionResponse.data || [],
|
tuitionData: (tuitionResponse.data || []),
|
||||||
};
|
};
|
||||||
|
|
||||||
setCareerDetails(updatedCareerDetails);
|
setCareerDetails(updatedCareerDetails);
|
||||||
@ -338,11 +373,33 @@ function Dashboard() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
[userState, apiUrl, areaTitle, userZipcode]
|
[userState, apiUrl, areaTitle, userZipcode, updateChatbotContext]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ============= Letting typed careers open PopoutPanel =============
|
||||||
|
// Called if the user picks a career in "CareerSearch" => { title, soc_code, cip_code }
|
||||||
|
const handleCareerFromSearch = useCallback((obj) => {
|
||||||
|
// Convert to shape used by handleCareerClick => { code, title, cipCode }
|
||||||
|
const adapted = {
|
||||||
|
code: obj.soc_code,
|
||||||
|
title: obj.title,
|
||||||
|
cipCode: obj.cip_code
|
||||||
|
};
|
||||||
|
console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted);
|
||||||
|
handleCareerClick(adapted);
|
||||||
|
}, [handleCareerClick]);
|
||||||
|
|
||||||
|
// If the user typed a career and clicked confirm
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingCareerForModal) {
|
||||||
|
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
|
||||||
|
handleCareerFromSearch(pendingCareerForModal);
|
||||||
|
setPendingCareerForModal(null);
|
||||||
|
}
|
||||||
|
}, [pendingCareerForModal, handleCareerFromSearch]);
|
||||||
|
|
||||||
|
// ============= RIASEC Chart Data =============
|
||||||
const chartData = {
|
const chartData = {
|
||||||
labels: riaSecScores.map((score) => score.area),
|
labels: riaSecScores.map((score) => score.area),
|
||||||
datasets: [
|
datasets: [
|
||||||
@ -356,10 +413,9 @@ function Dashboard() {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============= Hide the spinner if popout is open =============
|
||||||
const renderLoadingOverlay = () => {
|
const renderLoadingOverlay = () => {
|
||||||
// If we are NOT loading suggestions, or if the popout is visible, hide the overlay
|
|
||||||
if (!loadingSuggestions || popoutVisible) return null;
|
if (!loadingSuggestions || popoutVisible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
||||||
<div className="rounded bg-white p-6 shadow-lg">
|
<div className="rounded bg-white p-6 shadow-lg">
|
||||||
@ -380,31 +436,37 @@ function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
{showSessionExpiredModal && (
|
{showSessionExpiredModal && (
|
||||||
<div className="modal-overlay">
|
<div className="modal-overlay">
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
<h3>Session Expired</h3>
|
<h3>Session Expired</h3>
|
||||||
<p>Your session has expired or is invalid.</p>
|
<p>Your session has expired or is invalid.</p>
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button className="confirm-btn" onClick={() => setShowSessionExpiredModal(false)}>
|
<button className="confirm-btn" onClick={() => setShowSessionExpiredModal(false)}>
|
||||||
Stay Signed In
|
Stay Signed In
|
||||||
</button>
|
</button>
|
||||||
<button className="confirm-btn" onClick={() => {
|
<button
|
||||||
localStorage.removeItem("token");
|
className="confirm-btn"
|
||||||
localStorage.removeItem("UserId");
|
onClick={() => {
|
||||||
setShowSessionExpiredModal(false);
|
localStorage.removeItem("token");
|
||||||
navigate("/signin");
|
localStorage.removeItem("UserId");
|
||||||
}}>
|
setShowSessionExpiredModal(false);
|
||||||
Sign In Again
|
navigate("/signin");
|
||||||
</button>
|
}}
|
||||||
|
>
|
||||||
|
Sign In Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
{renderLoadingOverlay()}
|
||||||
{renderLoadingOverlay()}
|
|
||||||
|
|
||||||
<div className="dashboard-content">
|
<div className="dashboard-content">
|
||||||
|
{/* ====== 1) The new CareerSearch bar ====== */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Existing filters + suggestions */}
|
||||||
<div className="career-suggestions-container">
|
<div className="career-suggestions-container">
|
||||||
<div
|
<div
|
||||||
className="career-suggestions-header"
|
className="career-suggestions-header"
|
||||||
@ -443,6 +505,15 @@ function Dashboard() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<div style={{ marginLeft: 'auto' }}>
|
||||||
|
<CareerSearch
|
||||||
|
onCareerSelected={(careerObj) => {
|
||||||
|
console.log('[Dashboard] onCareerSelected =>', careerObj);
|
||||||
|
// Set the "pendingCareerForModal" so our useEffect fires below
|
||||||
|
setPendingCareerForModal(careerObj);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CareerSuggestions
|
<CareerSuggestions
|
||||||
careerSuggestions={memoizedCareerSuggestions}
|
careerSuggestions={memoizedCareerSuggestions}
|
||||||
@ -450,9 +521,11 @@ function Dashboard() {
|
|||||||
setLoading={setLoading}
|
setLoading={setLoading}
|
||||||
setProgress={setProgress}
|
setProgress={setProgress}
|
||||||
userState={userState}
|
userState={userState}
|
||||||
areaTitle={areaTitle}/>
|
areaTitle={areaTitle}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* RIASEC Container */}
|
||||||
<div className="riasec-container">
|
<div className="riasec-container">
|
||||||
<div className="riasec-scores">
|
<div className="riasec-scores">
|
||||||
<h2>RIASEC Scores</h2>
|
<h2>RIASEC Scores</h2>
|
||||||
@ -475,8 +548,10 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* The PopoutPanel */}
|
||||||
{memoizedPopoutPanel}
|
{memoizedPopoutPanel}
|
||||||
|
|
||||||
|
{/* Chatbot */}
|
||||||
<div className="chatbot-widget">
|
<div className="chatbot-widget">
|
||||||
{careerSuggestions.length > 0 ? (
|
{careerSuggestions.length > 0 ? (
|
||||||
<Chatbot context={chatbotContext} />
|
<Chatbot context={chatbotContext} />
|
||||||
@ -484,9 +559,7 @@ function Dashboard() {
|
|||||||
<p>Loading Chatbot...</p>
|
<p>Loading Chatbot...</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="data-source-acknowledgment"
|
className="data-source-acknowledgment"
|
||||||
style={{
|
style={{
|
||||||
@ -509,4 +582,4 @@ function Dashboard() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
@ -496,9 +496,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CareerSearch
|
<CareerSearch
|
||||||
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
|
onCareerSelected={(careerObj) => {
|
||||||
setPendingCareerForModal={setPendingCareerForModal}
|
setPendingCareerForModal(careerObj.title);
|
||||||
authFetch={authFetch}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScenarioEditModal
|
<ScenarioEditModal
|
||||||
@ -512,16 +512,19 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{pendingCareerForModal && (
|
{pendingCareerForModal && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Example Confirm
|
// Example: Actually adopt this career as a new scenario or update the DB
|
||||||
console.log('TODO: handleConfirmCareerSelection => new scenario?');
|
console.log('User confirmed new career path:', pendingCareerForModal);
|
||||||
|
// Perhaps you open another modal or POST to your API
|
||||||
|
// Then reset pendingCareerForModal:
|
||||||
|
setPendingCareerForModal(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm Career Change to {pendingCareerForModal}
|
Confirm Career Change to {pendingCareerForModal}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,8 +6,7 @@ const Paywall = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleSubscribe = () => {
|
const handleSubscribe = () => {
|
||||||
// Implement subscription logic here (Stripe, etc.)
|
navigate('/milestone-tracker');
|
||||||
alert('Subscription logic placeholder!');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -139,7 +139,7 @@ function PopoutPanel({
|
|||||||
`Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
|
`Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
|
||||||
);
|
);
|
||||||
if (decision) {
|
if (decision) {
|
||||||
navigate("/financial-profile", {
|
navigate("/paywall", {
|
||||||
state: {
|
state: {
|
||||||
selectedCareer: { career_path_id: match.id, career_name: data.title },
|
selectedCareer: { career_path_id: match.id, career_name: data.title },
|
||||||
},
|
},
|
||||||
|
@ -92,7 +92,7 @@ function UserProfile() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchProfileAndAreas();
|
fetchProfileAndAreas();
|
||||||
}, []);
|
}, []); // only runs once
|
||||||
|
|
||||||
const handleFormSubmit = async (e) => {
|
const handleFormSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -125,12 +125,59 @@ function UserProfile() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// FULL list of states, including all 50 states (+ DC if desired)
|
||||||
const states = [
|
const states = [
|
||||||
{ name: 'Alabama', code: 'AL' },
|
{ name: 'Alabama', code: 'AL' },
|
||||||
{ name: 'Alaska', code: 'AK' },
|
{ name: 'Alaska', code: 'AK' },
|
||||||
{ name: 'Arizona', code: 'AZ' },
|
{ name: 'Arizona', code: 'AZ' },
|
||||||
// ... (truncated for brevity, include all states)
|
{ name: 'Arkansas', code: 'AR' },
|
||||||
{ name: 'Wyoming', code: 'WY' }
|
{ name: 'California', code: 'CA' },
|
||||||
|
{ name: 'Colorado', code: 'CO' },
|
||||||
|
{ name: 'Connecticut', code: 'CT' },
|
||||||
|
{ name: 'Delaware', code: 'DE' },
|
||||||
|
{ name: 'District of Columbia', code: 'DC' },
|
||||||
|
{ name: 'Florida', code: 'FL' },
|
||||||
|
{ name: 'Georgia', code: 'GA' },
|
||||||
|
{ name: 'Hawaii', code: 'HI' },
|
||||||
|
{ name: 'Idaho', code: 'ID' },
|
||||||
|
{ name: 'Illinois', code: 'IL' },
|
||||||
|
{ name: 'Indiana', code: 'IN' },
|
||||||
|
{ name: 'Iowa', code: 'IA' },
|
||||||
|
{ name: 'Kansas', code: 'KS' },
|
||||||
|
{ name: 'Kentucky', code: 'KY' },
|
||||||
|
{ name: 'Louisiana', code: 'LA' },
|
||||||
|
{ name: 'Maine', code: 'ME' },
|
||||||
|
{ name: 'Maryland', code: 'MD' },
|
||||||
|
{ name: 'Massachusetts', code: 'MA' },
|
||||||
|
{ name: 'Michigan', code: 'MI' },
|
||||||
|
{ name: 'Minnesota', code: 'MN' },
|
||||||
|
{ name: 'Mississippi', code: 'MS' },
|
||||||
|
{ name: 'Missouri', code: 'MO' },
|
||||||
|
{ name: 'Montana', code: 'MT' },
|
||||||
|
{ name: 'Nebraska', code: 'NE' },
|
||||||
|
{ name: 'Nevada', code: 'NV' },
|
||||||
|
{ name: 'New Hampshire', code: 'NH' },
|
||||||
|
{ name: 'New Jersey', code: 'NJ' },
|
||||||
|
{ name: 'New Mexico', code: 'NM' },
|
||||||
|
{ name: 'New York', code: 'NY' },
|
||||||
|
{ name: 'North Carolina', code: 'NC' },
|
||||||
|
{ name: 'North Dakota', code: 'ND' },
|
||||||
|
{ name: 'Ohio', code: 'OH' },
|
||||||
|
{ name: 'Oklahoma', code: 'OK' },
|
||||||
|
{ name: 'Oregon', code: 'OR' },
|
||||||
|
{ name: 'Pennsylvania', code: 'PA' },
|
||||||
|
{ name: 'Rhode Island', code: 'RI' },
|
||||||
|
{ name: 'South Carolina', code: 'SC' },
|
||||||
|
{ name: 'South Dakota', code: 'SD' },
|
||||||
|
{ name: 'Tennessee', code: 'TN' },
|
||||||
|
{ name: 'Texas', code: 'TX' },
|
||||||
|
{ name: 'Utah', code: 'UT' },
|
||||||
|
{ name: 'Vermont', code: 'VT' },
|
||||||
|
{ name: 'Virginia', code: 'VA' },
|
||||||
|
{ name: 'Washington', code: 'WA' },
|
||||||
|
{ name: 'West Virginia', code: 'WV' },
|
||||||
|
{ name: 'Wisconsin', code: 'WI' },
|
||||||
|
{ name: 'Wyoming', code: 'WY' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -195,7 +242,7 @@ function UserProfile() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* State */}
|
{/* State Dropdown */}
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||||
State:
|
State:
|
||||||
|
BIN
upser_profile.db
BIN
upser_profile.db
Binary file not shown.
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user