Fixed navigation issue in Dashboard where it reverted to INterest Inventory
This commit is contained in:
parent
34fda5760d
commit
54d0fcb4e6
@ -3,13 +3,21 @@
|
||||
import axios from 'axios';
|
||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
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 PopoutPanel from './PopoutPanel.js';
|
||||
import MilestoneTracker from './MilestoneTracker.js';
|
||||
import CareerSearch from './CareerSearch.js'; // <--- Import your new search
|
||||
import Chatbot from "./Chatbot.js";
|
||||
import CareerSearch from './CareerSearch.js'; // <--- Import your new search
|
||||
import Chatbot from './Chatbot.js';
|
||||
|
||||
import './Dashboard.css';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
@ -51,12 +59,10 @@ function Dashboard() {
|
||||
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;
|
||||
// We'll consider the popout panel visible if there's a selectedCareer
|
||||
const popoutVisible = !!selectedCareer;
|
||||
|
||||
// ============= Auth & URL Setup =============
|
||||
@ -64,9 +70,9 @@ function Dashboard() {
|
||||
|
||||
// AUTH fetch
|
||||
const authFetch = async (url, options = {}, onUnauthorized) => {
|
||||
const token = localStorage.getItem("token");
|
||||
const token = localStorage.getItem('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();
|
||||
return null;
|
||||
}
|
||||
@ -79,21 +85,21 @@ function Dashboard() {
|
||||
};
|
||||
try {
|
||||
const res = await fetch(url, finalOptions);
|
||||
console.log("Response Status:", res.status);
|
||||
console.log('Response Status:', res.status);
|
||||
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();
|
||||
return null;
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error("Fetch error:", err);
|
||||
console.error('Fetch error:', err);
|
||||
if (typeof onUnauthorized === 'function') onUnauthorized();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// ============= User Profile Fetch =============
|
||||
// ============= Fetch user profile =============
|
||||
const fetchUserProfile = async () => {
|
||||
const res = await authFetch(`${apiUrl}/user-profile`);
|
||||
if (!res) return;
|
||||
@ -103,32 +109,93 @@ function Dashboard() {
|
||||
setUserState(profileData.state);
|
||||
setAreaTitle(profileData.area.trim() || '');
|
||||
setUserZipcode(profileData.zipcode);
|
||||
// Store entire userProfile if needed
|
||||
setUserProfile(profileData);
|
||||
} else {
|
||||
console.error('Failed to fetch user profile');
|
||||
}
|
||||
};
|
||||
|
||||
// ============= Lifecycle: Load Profile, Setup =============
|
||||
// We'll store the userProfile for reference
|
||||
const [userProfile, setUserProfile] = useState(null);
|
||||
|
||||
// ============= Lifecycle: Load Profile =============
|
||||
useEffect(() => {
|
||||
fetchUserProfile();
|
||||
}, [apiUrl]); // load once
|
||||
|
||||
// ============= jobZone & fit Setup =============
|
||||
// ============= jobZone & Fit Setup =============
|
||||
const jobZoneLabels = {
|
||||
'1': 'Little or No Preparation',
|
||||
'2': 'Some Preparation Needed',
|
||||
'3': 'Medium Preparation Needed',
|
||||
'4': 'Considerable Preparation Needed',
|
||||
'5': 'Extensive Preparation Needed'
|
||||
'5': 'Extensive Preparation Needed',
|
||||
};
|
||||
|
||||
const fitLabels = {
|
||||
'Best': 'Best - Very Strong Match',
|
||||
'Great': 'Great - Strong Match',
|
||||
'Good': 'Good - Less Strong Match'
|
||||
Best: 'Best - Very Strong Match',
|
||||
Great: 'Great - Strong Match',
|
||||
Good: 'Good - Less Strong Match',
|
||||
};
|
||||
|
||||
// Fetch job zones for each career suggestion
|
||||
// ============= "Mimic" InterestInventory submission if user has 60 answers =============
|
||||
const mimicInterestInventorySubmission = async (answers) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setProgress(0);
|
||||
const response = await authFetch(`${apiUrl}/onet/submit_answers`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ answers }),
|
||||
});
|
||||
if (!response || !response.ok) {
|
||||
throw new Error('Failed to submit stored answers');
|
||||
}
|
||||
const data = await response.json();
|
||||
const { careers, riaSecScores } = data;
|
||||
|
||||
// This sets location.state, so the next effect sees it as if we came from InterestInventory
|
||||
navigate('/dashboard', {
|
||||
state: { careerSuggestions: careers, riaSecScores },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error mimicking submission:', err);
|
||||
alert('We could not load your saved answers. Please retake the Interest Inventory.');
|
||||
navigate('/interest-inventory');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ============= On Page Load: get careerSuggestions from location.state, or mimic =============
|
||||
useEffect(() => {
|
||||
// If we have location.state from InterestInventory, proceed as normal
|
||||
if (location.state) {
|
||||
let descriptions = [];
|
||||
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
|
||||
descriptions = (scores || []).map((score) => score.description || 'No description available.');
|
||||
setCareerSuggestions(suggestions || []);
|
||||
setRiaSecScores(scores || []);
|
||||
setRiaSecDescriptions(descriptions);
|
||||
} else {
|
||||
// We came here directly; wait for userProfile, then check answers
|
||||
if (!userProfile) return; // wait until userProfile is loaded
|
||||
|
||||
const storedAnswers = userProfile.interest_inventory_answers;
|
||||
if (storedAnswers && storedAnswers.length === 60) {
|
||||
// Mimic the submission so we get suggestions
|
||||
mimicInterestInventorySubmission(storedAnswers);
|
||||
} else {
|
||||
alert(
|
||||
'We need your Interest Inventory answers to generate career suggestions. Redirecting...'
|
||||
);
|
||||
navigate('/interest-inventory');
|
||||
}
|
||||
}
|
||||
}, [location.state, navigate, userProfile]);
|
||||
|
||||
// ============= jobZone fetch =============
|
||||
useEffect(() => {
|
||||
const fetchJobZones = async () => {
|
||||
if (careerSuggestions.length === 0) return;
|
||||
@ -148,7 +215,7 @@ function Dashboard() {
|
||||
fetchJobZones();
|
||||
}, [careerSuggestions, apiUrl]);
|
||||
|
||||
// Filter careers by job zone, fit
|
||||
// ============= Filter by job zone, fit =============
|
||||
const filteredCareers = useMemo(() => {
|
||||
return careersWithJobZone.filter((career) => {
|
||||
const jobZoneMatches = selectedJobZone
|
||||
@ -163,7 +230,7 @@ function Dashboard() {
|
||||
});
|
||||
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
||||
|
||||
// Merge updated data into chatbot context
|
||||
// ============= Merge data into chatbot context =============
|
||||
const updateChatbotContext = (updatedData) => {
|
||||
setChatbotContext((prevContext) => {
|
||||
const mergedContext = {
|
||||
@ -179,56 +246,6 @@ function Dashboard() {
|
||||
});
|
||||
};
|
||||
|
||||
// Our final array for CareerSuggestions
|
||||
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
|
||||
|
||||
// ============= Popout Panel Setup =============
|
||||
const memoizedPopoutPanel = useMemo(() => {
|
||||
return (
|
||||
<PopoutPanel
|
||||
isVisible={!!selectedCareer}
|
||||
data={careerDetails}
|
||||
schools={schools}
|
||||
salaryData={salaryData}
|
||||
economicProjections={economicProjections}
|
||||
tuitionData={tuitionData}
|
||||
closePanel={() => setSelectedCareer(null)}
|
||||
loading={loading}
|
||||
error={error}
|
||||
userState={userState}
|
||||
results={results}
|
||||
updateChatbotContext={updateChatbotContext}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
selectedCareer,
|
||||
careerDetails,
|
||||
schools,
|
||||
salaryData,
|
||||
economicProjections,
|
||||
tuitionData,
|
||||
loading,
|
||||
error,
|
||||
userState,
|
||||
results
|
||||
]);
|
||||
|
||||
// ============= On Page Load: get careerSuggestions from location.state, etc. =============
|
||||
useEffect(() => {
|
||||
let descriptions = [];
|
||||
if (location.state) {
|
||||
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
|
||||
descriptions = (scores || []).map((score) => score.description || "No description available.");
|
||||
setCareerSuggestions(suggestions || []);
|
||||
setRiaSecScores(scores || []);
|
||||
setRiaSecDescriptions(descriptions);
|
||||
} else {
|
||||
console.warn('No data found, redirecting to Interest Inventory');
|
||||
navigate('/interest-inventory');
|
||||
}
|
||||
}, [location.state, navigate]);
|
||||
|
||||
// Once userState, areaTitle, userZipcode, etc. are set, update chatbot
|
||||
useEffect(() => {
|
||||
if (
|
||||
careerSuggestions.length > 0 &&
|
||||
@ -240,15 +257,15 @@ function Dashboard() {
|
||||
const newChatbotContext = {
|
||||
careerSuggestions: [...careersWithJobZone],
|
||||
riaSecScores: [...riaSecScores],
|
||||
userState: userState || "",
|
||||
areaTitle: areaTitle || "",
|
||||
userZipcode: userZipcode || "",
|
||||
userState: userState || '',
|
||||
areaTitle: areaTitle || '',
|
||||
userZipcode: userZipcode || '',
|
||||
};
|
||||
setChatbotContext(newChatbotContext);
|
||||
}
|
||||
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
|
||||
|
||||
// ============= handleCareerClick (for tile clicks) =============
|
||||
// ============= handleCareerClick =============
|
||||
const handleCareerClick = useCallback(
|
||||
async (career) => {
|
||||
console.log('[handleCareerClick] career =>', career);
|
||||
@ -284,7 +301,9 @@ function Dashboard() {
|
||||
// Salary
|
||||
let salaryResponse;
|
||||
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 },
|
||||
});
|
||||
} catch (error) {
|
||||
salaryResponse = { data: {} };
|
||||
}
|
||||
@ -300,73 +319,77 @@ function Dashboard() {
|
||||
// Tuition
|
||||
let tuitionResponse;
|
||||
try {
|
||||
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } });
|
||||
tuitionResponse = await axios.get(`${apiUrl}/tuition`, {
|
||||
params: { cipCode: cleanedCipCode, state: userState },
|
||||
});
|
||||
} catch (error) {
|
||||
tuitionResponse = { data: {} };
|
||||
}
|
||||
|
||||
// Fetch schools
|
||||
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
||||
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||
try {
|
||||
const response = await axios.post(`${apiUrl}/maps/distance`, {
|
||||
userZipcode,
|
||||
destinations: schoolAddress,
|
||||
});
|
||||
const { distance, duration } = response.data;
|
||||
return { ...school, distance, duration };
|
||||
} catch (error) {
|
||||
return { ...school, distance: 'N/A', duration: 'N/A' };
|
||||
}
|
||||
}));
|
||||
const schoolsWithDistance = await Promise.all(
|
||||
filteredSchools.map(async (school) => {
|
||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||
try {
|
||||
const response = await axios.post(`${apiUrl}/maps/distance`, {
|
||||
userZipcode,
|
||||
destinations: schoolAddress,
|
||||
});
|
||||
const { distance, duration } = response.data;
|
||||
return { ...school, distance, duration };
|
||||
} catch (error) {
|
||||
return { ...school, distance: 'N/A', duration: 'N/A' };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Build salary array
|
||||
const sData = salaryResponse.data || {};
|
||||
const salaryDataPoints = sData && Object.keys(sData).length > 0
|
||||
? [
|
||||
{
|
||||
percentile: "10th Percentile",
|
||||
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
|
||||
nationalSalary: parseInt(sData.national?.national_PCT10, 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
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const salaryDataPoints =
|
||||
sData && Object.keys(sData).length > 0
|
||||
? [
|
||||
{
|
||||
percentile: '10th Percentile',
|
||||
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
|
||||
nationalSalary: parseInt(sData.national?.national_PCT10, 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 = {
|
||||
...career,
|
||||
jobDescription: description,
|
||||
tasks: tasks,
|
||||
economicProjections: (economicResponse.data || {}),
|
||||
economicProjections: economicResponse.data || {},
|
||||
salaryData: salaryDataPoints,
|
||||
schools: schoolsWithDistance,
|
||||
tuitionData: (tuitionResponse.data || []),
|
||||
tuitionData: tuitionResponse.data || [],
|
||||
};
|
||||
|
||||
setCareerDetails(updatedCareerDetails);
|
||||
updateChatbotContext({ careerDetails: updatedCareerDetails });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing career click:', error.message);
|
||||
setError('Failed to load data');
|
||||
@ -377,20 +400,20 @@ function Dashboard() {
|
||||
[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]);
|
||||
// ============= Let typed careers open PopoutPanel =============
|
||||
const handleCareerFromSearch = useCallback(
|
||||
(obj) => {
|
||||
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);
|
||||
@ -433,6 +456,38 @@ function Dashboard() {
|
||||
);
|
||||
};
|
||||
|
||||
// ============= Popout Panel Setup =============
|
||||
const memoizedPopoutPanel = useMemo(() => {
|
||||
return (
|
||||
<PopoutPanel
|
||||
isVisible={!!selectedCareer}
|
||||
data={careerDetails}
|
||||
schools={schools}
|
||||
salaryData={salaryData}
|
||||
economicProjections={economicProjections}
|
||||
tuitionData={tuitionData}
|
||||
closePanel={() => setSelectedCareer(null)}
|
||||
loading={loading}
|
||||
error={error}
|
||||
userState={userState}
|
||||
results={results}
|
||||
updateChatbotContext={updateChatbotContext}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
selectedCareer,
|
||||
careerDetails,
|
||||
schools,
|
||||
salaryData,
|
||||
economicProjections,
|
||||
tuitionData,
|
||||
loading,
|
||||
error,
|
||||
userState,
|
||||
results,
|
||||
updateChatbotContext,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
{showSessionExpiredModal && (
|
||||
@ -441,16 +496,19 @@ function Dashboard() {
|
||||
<h3>Session Expired</h3>
|
||||
<p>Your session has expired or is invalid.</p>
|
||||
<div className="modal-actions">
|
||||
<button className="confirm-btn" onClick={() => setShowSessionExpiredModal(false)}>
|
||||
<button
|
||||
className="confirm-btn"
|
||||
onClick={() => setShowSessionExpiredModal(false)}
|
||||
>
|
||||
Stay Signed In
|
||||
</button>
|
||||
<button
|
||||
className="confirm-btn"
|
||||
onClick={() => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("UserId");
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('UserId');
|
||||
setShowSessionExpiredModal(false);
|
||||
navigate("/signin");
|
||||
navigate('/signin');
|
||||
}}
|
||||
>
|
||||
Sign In Again
|
||||
@ -465,7 +523,6 @@ function Dashboard() {
|
||||
<div className="dashboard-content">
|
||||
{/* ====== 1) The new CareerSearch bar ====== */}
|
||||
|
||||
|
||||
{/* Existing filters + suggestions */}
|
||||
<div className="career-suggestions-container">
|
||||
<div
|
||||
@ -475,7 +532,7 @@ function Dashboard() {
|
||||
alignItems: 'center',
|
||||
marginBottom: '15px',
|
||||
justifyContent: 'center',
|
||||
gap: '15px'
|
||||
gap: '15px',
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
@ -487,7 +544,9 @@ function Dashboard() {
|
||||
>
|
||||
<option value="">All Preparation Levels</option>
|
||||
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||
<option key={zone} value={zone}>{label}</option>
|
||||
<option key={zone} value={zone}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
@ -501,7 +560,9 @@ function Dashboard() {
|
||||
>
|
||||
<option value="">All Fit Levels</option>
|
||||
{Object.entries(fitLabels).map(([key, label]) => (
|
||||
<option key={key} value={key}>{label}</option>
|
||||
<option key={key} value={key}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
@ -509,14 +570,15 @@ function Dashboard() {
|
||||
<CareerSearch
|
||||
onCareerSelected={(careerObj) => {
|
||||
console.log('[Dashboard] onCareerSelected =>', careerObj);
|
||||
// Set the "pendingCareerForModal" so our useEffect fires below
|
||||
// Set the "pendingCareerForModal" so our useEffect fires
|
||||
setPendingCareerForModal(careerObj);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CareerSuggestions
|
||||
careerSuggestions={memoizedCareerSuggestions}
|
||||
careerSuggestions={filteredCareers}
|
||||
onCareerClick={handleCareerClick}
|
||||
setLoading={setLoading}
|
||||
setProgress={setProgress}
|
||||
@ -568,14 +630,38 @@ function Dashboard() {
|
||||
borderTop: '1px solid #ccc',
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
textAlign: 'center'
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
Career results and RIASEC scores are provided by
|
||||
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>, in conjunction with the
|
||||
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer"> Bureau of Labor Statistics</a>, and the
|
||||
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer"> National Center for Education Statistics (NCES)</a>.
|
||||
<a
|
||||
href="https://www.onetcenter.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{' '}
|
||||
O*Net
|
||||
</a>
|
||||
, in conjunction with the
|
||||
<a
|
||||
href="https://www.bls.gov"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{' '}
|
||||
Bureau of Labor Statistics
|
||||
</a>
|
||||
, and the
|
||||
<a
|
||||
href="https://nces.ed.gov"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{' '}
|
||||
National Center for Education Statistics (NCES)
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user