CareerExplorer build out
This commit is contained in:
parent
f9674643d5
commit
0194e9fc19
@ -144,38 +144,7 @@ app.post('/api/register', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Route for login
|
// Route to handle user sign-in (updated with career_priorities and career_list)
|
||||||
app.post('/api/login', (req, res) => {
|
|
||||||
const { username, password } = req.body;
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
return res.status(400).json({ error: 'Username and password are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = 'SELECT * FROM user_auth WHERE username = ?';
|
|
||||||
db.get(query, [username], async (err, row) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error fetching user:', err.message);
|
|
||||||
return res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
return res.status(401).json({ error: 'Invalid username or password' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify password
|
|
||||||
const isPasswordValid = await bcrypt.compare(password, row.hashed_password);
|
|
||||||
if (!isPasswordValid) {
|
|
||||||
return res.status(401).json({ error: 'Invalid username or password' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { user_id } = row; // This gets the correct user_id from the row object
|
|
||||||
const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '2h' });
|
|
||||||
res.status(200).json({ token });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Route to handle user sign-in (customized)
|
|
||||||
app.post('/api/signin', async (req, res) => {
|
app.post('/api/signin', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
@ -183,7 +152,7 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Both username and password are required' });
|
return res.status(400).json({ error: 'Both username and password are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// JOIN user_profile to fetch is_premium, email, or whatever columns you need
|
// Updated query includes career_priorities and career_list
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
user_auth.user_id,
|
user_auth.user_id,
|
||||||
@ -194,7 +163,9 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
user_profile.career_situation,
|
user_profile.career_situation,
|
||||||
user_profile.email,
|
user_profile.email,
|
||||||
user_profile.firstname,
|
user_profile.firstname,
|
||||||
user_profile.lastname
|
user_profile.lastname,
|
||||||
|
user_profile.career_priorities, -- new field
|
||||||
|
user_profile.career_list -- new field
|
||||||
FROM user_auth
|
FROM user_auth
|
||||||
LEFT JOIN user_profile ON user_auth.user_id = user_profile.user_id
|
LEFT JOIN user_profile ON user_auth.user_id = user_profile.user_id
|
||||||
WHERE user_auth.username = ?
|
WHERE user_auth.username = ?
|
||||||
@ -217,20 +188,16 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
return res.status(401).json({ error: 'Invalid username or password' });
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// user_id from the row
|
|
||||||
const { user_id } = row;
|
|
||||||
|
|
||||||
// Generate JWT
|
// Generate JWT
|
||||||
const token = jwt.sign({ userId: user_id }, SECRET_KEY, { expiresIn: '2h' });
|
const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '2h' });
|
||||||
|
|
||||||
// Return user object including is_premium and other columns
|
// Return fully updated user object
|
||||||
// The front end can store this in state (e.g. setUser).
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: 'Login successful',
|
message: 'Login successful',
|
||||||
token,
|
token,
|
||||||
userId: user_id,
|
userId: row.user_id,
|
||||||
user: {
|
user: {
|
||||||
user_id,
|
user_id: row.user_id,
|
||||||
firstname: row.firstname,
|
firstname: row.firstname,
|
||||||
lastname: row.lastname,
|
lastname: row.lastname,
|
||||||
email: row.email,
|
email: row.email,
|
||||||
@ -238,11 +205,14 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
is_premium: row.is_premium,
|
is_premium: row.is_premium,
|
||||||
is_pro_premium: row.is_pro_premium,
|
is_pro_premium: row.is_pro_premium,
|
||||||
career_situation: row.career_situation,
|
career_situation: row.career_situation,
|
||||||
|
career_priorities: row.career_priorities, // newly added
|
||||||
|
career_list: row.career_list, // newly added
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/// Check if username already exists
|
/// Check if username already exists
|
||||||
app.get('/api/check-username/:username', (req, res) => {
|
app.get('/api/check-username/:username', (req, res) => {
|
||||||
const { username } = req.params;
|
const { username } = req.params;
|
||||||
@ -263,6 +233,129 @@ app.get('/api/check-username/:username', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Upsert user profile with career_priorities and career_list
|
||||||
|
app.post('/api/user-profile', (req, res) => {
|
||||||
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Authorization token is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let userId;
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, SECRET_KEY);
|
||||||
|
userId = decoded.userId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JWT verification failed:', error);
|
||||||
|
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
zipCode,
|
||||||
|
state,
|
||||||
|
area,
|
||||||
|
careerSituation,
|
||||||
|
interest_inventory_answers,
|
||||||
|
career_priorities,
|
||||||
|
career_list,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!firstName || !lastName || !email || !zipCode || !state || !area) {
|
||||||
|
return res.status(400).json({ error: 'All fields are required (except interest_inventory_answers, career_priorities, career_list)' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing user profile
|
||||||
|
const checkQuery = `SELECT * FROM user_profile WHERE user_id = ?;`;
|
||||||
|
db.get(checkQuery, [userId], (err, existingRow) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error checking profile:', err.message);
|
||||||
|
return res.status(500).json({ error: 'Database error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve existing data if not provided
|
||||||
|
const finalAnswers = interest_inventory_answers === undefined
|
||||||
|
? existingRow?.interest_inventory_answers || null
|
||||||
|
: interest_inventory_answers;
|
||||||
|
|
||||||
|
const finalCareerPriorities = career_priorities === undefined
|
||||||
|
? existingRow?.career_priorities || null
|
||||||
|
: career_priorities;
|
||||||
|
|
||||||
|
const finalCareerList = career_list === undefined
|
||||||
|
? existingRow?.career_list || null
|
||||||
|
: career_list;
|
||||||
|
|
||||||
|
if (existingRow) {
|
||||||
|
// Update existing profile
|
||||||
|
const updateQuery = `
|
||||||
|
UPDATE user_profile
|
||||||
|
SET firstname = ?,
|
||||||
|
lastname = ?,
|
||||||
|
email = ?,
|
||||||
|
zipcode = ?,
|
||||||
|
state = ?,
|
||||||
|
area = ?,
|
||||||
|
career_situation = ?,
|
||||||
|
interest_inventory_answers = ?,
|
||||||
|
career_priorities = ?,
|
||||||
|
career_list = ?
|
||||||
|
WHERE user_id = ?
|
||||||
|
`;
|
||||||
|
const params = [
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
zipCode,
|
||||||
|
state,
|
||||||
|
area,
|
||||||
|
careerSituation || existingRow.career_situation,
|
||||||
|
finalAnswers,
|
||||||
|
finalCareerPriorities,
|
||||||
|
finalCareerList,
|
||||||
|
userId,
|
||||||
|
];
|
||||||
|
db.run(updateQuery, params, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error updating profile:', err.message);
|
||||||
|
return res.status(500).json({ error: 'Failed to update user profile' });
|
||||||
|
}
|
||||||
|
return res.status(200).json({ message: 'User profile updated successfully' });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Insert new profile
|
||||||
|
const insertQuery = `
|
||||||
|
INSERT INTO user_profile
|
||||||
|
(firstname, lastname, email, zipcode, state, area, career_situation, interest_inventory_answers, career_priorities, career_list, user_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
const params = [
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
zipCode,
|
||||||
|
state,
|
||||||
|
area,
|
||||||
|
careerSituation || null,
|
||||||
|
finalAnswers,
|
||||||
|
finalCareerPriorities,
|
||||||
|
finalCareerList,
|
||||||
|
userId,
|
||||||
|
];
|
||||||
|
db.run(insertQuery, params, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting profile:', err.message);
|
||||||
|
return res.status(500).json({ error: 'Failed to create user profile' });
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
.status(201)
|
||||||
|
.json({ message: 'User profile created successfully', id: this.lastID });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Route to fetch user profile
|
// Route to fetch user profile
|
||||||
|
17379
final_master_career_list.json
Normal file
17379
final_master_career_list.json
Normal file
File diff suppressed because it is too large
Load Diff
31707
public/careers_with_ratings.json
Normal file
31707
public/careers_with_ratings.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,8 @@ import GettingStarted from './components/GettingStarted.js';
|
|||||||
import SignIn from './components/SignIn.js';
|
import SignIn from './components/SignIn.js';
|
||||||
import SignUp from './components/SignUp.js';
|
import SignUp from './components/SignUp.js';
|
||||||
import PlanningLanding from './components/PlanningLanding.js';
|
import PlanningLanding from './components/PlanningLanding.js';
|
||||||
|
import CareerExplorer from './components/CareerExplorer.js';
|
||||||
|
import EducationalPrograms from './components/EducationalPrograms.js';
|
||||||
import PreparingLanding from './components/PreparingLanding.js';
|
import PreparingLanding from './components/PreparingLanding.js';
|
||||||
import EnhancingLanding from './components/EnhancingLanding.js';
|
import EnhancingLanding from './components/EnhancingLanding.js';
|
||||||
import RetirementLanding from './components/RetirementLanding.js';
|
import RetirementLanding from './components/RetirementLanding.js';
|
||||||
@ -236,6 +238,8 @@ function App() {
|
|||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/profile" element={<UserProfile />} />
|
<Route path="/profile" element={<UserProfile />} />
|
||||||
<Route path="/planning" element={<PlanningLanding />} />
|
<Route path="/planning" element={<PlanningLanding />} />
|
||||||
|
<Route path="/career-explorer" element={<CareerExplorer />} />
|
||||||
|
<Route path="/educational-programs" element={<EducationalPrograms />} />
|
||||||
<Route path="/preparing" element={<PreparingLanding />} />
|
<Route path="/preparing" element={<PreparingLanding />} />
|
||||||
<Route path="/enhancing" element={<EnhancingLanding />} />
|
<Route path="/enhancing" element={<EnhancingLanding />} />
|
||||||
<Route path="/retirement" element={<RetirementLanding />} />
|
<Route path="/retirement" element={<RetirementLanding />} />
|
||||||
|
367
src/components/CareerExplorer.js
Normal file
367
src/components/CareerExplorer.js
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import CareerSuggestions from './CareerSuggestions.js';
|
||||||
|
import CareerPrioritiesModal from './CareerPrioritiesModal.js';
|
||||||
|
import CareerModal from './CareerModal.js';
|
||||||
|
import CareerSearch from './CareerSearch.js';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
|
const [userProfile, setUserProfile] = useState(null);
|
||||||
|
const [masterCareerRatings, setMasterCareerRatings] = useState([]);
|
||||||
|
const [careerList, setCareerList] = useState([]);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
||||||
|
const [careersWithJobZone, setCareersWithJobZone] = useState([]);
|
||||||
|
const [selectedJobZone, setSelectedJobZone] = useState('');
|
||||||
|
const [selectedFit, setSelectedFit] = useState('');
|
||||||
|
const [selectedCareer, setSelectedCareer] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
const jobZoneLabels = {
|
||||||
|
'1': 'Little or No Preparation',
|
||||||
|
'2': 'Some Preparation Needed',
|
||||||
|
'3': 'Medium Preparation Needed',
|
||||||
|
'4': 'Considerable Preparation Needed',
|
||||||
|
'5': 'Extensive Preparation Needed',
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitLabels = {
|
||||||
|
Best: 'Best - Very Strong Match',
|
||||||
|
Great: 'Great - Strong Match',
|
||||||
|
Good: 'Good - Less Strong Match',
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/careers_with_ratings.json')
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch ratings JSON');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((data) => setMasterCareerRatings(data))
|
||||||
|
.catch((err) => console.error('Error fetching career ratings:', err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const priorities = useMemo(() => {
|
||||||
|
return userProfile?.career_priorities ? JSON.parse(userProfile.career_priorities) : {};
|
||||||
|
}, [userProfile]);
|
||||||
|
|
||||||
|
const priorityKeys = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'];
|
||||||
|
|
||||||
|
const getCareerRatingsBySocCode = (socCode) => {
|
||||||
|
return masterCareerRatings.find(c => c.soc_code === socCode)?.ratings || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const addCareerToList = (career) => {
|
||||||
|
const masterRatings = getCareerRatingsBySocCode(career.code);
|
||||||
|
|
||||||
|
const fitRatingMap = {
|
||||||
|
Best: 5,
|
||||||
|
Great: 4,
|
||||||
|
Good: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const interestsRating = priorities.interests === 'I’m not sure yet'
|
||||||
|
? parseInt(prompt('Rate your interest in this career (1-5):', '3'), 10)
|
||||||
|
: fitRatingMap[career.fit] || masterRatings.interests || 3;
|
||||||
|
|
||||||
|
const meaningRating = parseInt(prompt('How meaningful is this career to you? (1-5):', '3'), 10);
|
||||||
|
|
||||||
|
const stabilityRating = career.ratings && career.ratings.stability !== undefined
|
||||||
|
? career.ratings.stability
|
||||||
|
: masterRatings.stability || 3;
|
||||||
|
|
||||||
|
const growthRating = masterRatings.growth || 3;
|
||||||
|
const balanceRating = masterRatings.balance || 3;
|
||||||
|
const recognitionRating = masterRatings.recognition || 3;
|
||||||
|
|
||||||
|
const careerWithUserRatings = {
|
||||||
|
...career,
|
||||||
|
ratings: {
|
||||||
|
interests: interestsRating,
|
||||||
|
meaning: meaningRating,
|
||||||
|
stability: stabilityRating,
|
||||||
|
growth: growthRating,
|
||||||
|
balance: balanceRating,
|
||||||
|
recognition: recognitionRating,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setCareerList((prevList) => {
|
||||||
|
if (prevList.some((c) => c.code === career.code)) {
|
||||||
|
alert('Career already in comparison list.');
|
||||||
|
return prevList;
|
||||||
|
}
|
||||||
|
return [...prevList, careerWithUserRatings];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCareerFromList = (careerCode) => {
|
||||||
|
setCareerList((prevList) => prevList.filter((c) => c.code !== careerCode));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserProfile = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const res = await axios.get(`${apiUrl}/user-profile`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
const profileData = res.data;
|
||||||
|
setUserProfile(profileData);
|
||||||
|
|
||||||
|
const priorities = profileData.career_priorities
|
||||||
|
? JSON.parse(profileData.career_priorities)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const allAnswered = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'].every(
|
||||||
|
(key) => priorities[key]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allAnswered) {
|
||||||
|
setShowModal(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setShowModal(true); // profile not found, show modal by default
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching user profile:', err);
|
||||||
|
setShowModal(true); // on error, default to showing modal
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserProfile();
|
||||||
|
}, [apiUrl]);
|
||||||
|
|
||||||
|
// Load suggestions from Interest Inventory if provided (optional)
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.state?.careerSuggestions) {
|
||||||
|
setCareerSuggestions(location.state.careerSuggestions);
|
||||||
|
}
|
||||||
|
}, [location.state]);
|
||||||
|
|
||||||
|
// Fetch Job Zones if suggestions are provided
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchJobZones = async () => {
|
||||||
|
if (!careerSuggestions.length) return;
|
||||||
|
|
||||||
|
const flatSuggestions = careerSuggestions.flat();
|
||||||
|
const socCodes = flatSuggestions.map((career) => career.code);
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
|
||||||
|
const jobZoneData = response.data;
|
||||||
|
const updatedCareers = flatSuggestions.map((career) => ({
|
||||||
|
...career,
|
||||||
|
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
||||||
|
}));
|
||||||
|
setCareersWithJobZone(updatedCareers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching job zone information:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchJobZones();
|
||||||
|
}, [careerSuggestions, apiUrl]);
|
||||||
|
|
||||||
|
// Filtering logic (Job Zone and Fit)
|
||||||
|
const filteredCareers = useMemo(() => {
|
||||||
|
return careersWithJobZone.filter((career) => {
|
||||||
|
const jobZoneMatches = selectedJobZone
|
||||||
|
? career.job_zone !== null &&
|
||||||
|
career.job_zone !== undefined &&
|
||||||
|
Number(career.job_zone) === Number(selectedJobZone)
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const fitMatches = selectedFit ? career.fit === selectedFit : true;
|
||||||
|
return jobZoneMatches && fitMatches;
|
||||||
|
});
|
||||||
|
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
||||||
|
|
||||||
|
const priorityWeight = (priority, response) => {
|
||||||
|
const weightMap = {
|
||||||
|
interests: {
|
||||||
|
'I know my interests (completed inventory)': 5,
|
||||||
|
'I’m not sure yet': 1,
|
||||||
|
},
|
||||||
|
meaning: {
|
||||||
|
'Yes, very important': 5,
|
||||||
|
'Somewhat important': 3,
|
||||||
|
'Not as important': 1,
|
||||||
|
},
|
||||||
|
stability: {
|
||||||
|
'Very important': 5,
|
||||||
|
'Somewhat important': 3,
|
||||||
|
'Not as important': 1,
|
||||||
|
},
|
||||||
|
growth: {
|
||||||
|
'Yes, very important': 5,
|
||||||
|
'Somewhat important': 3,
|
||||||
|
'Not as important': 1,
|
||||||
|
},
|
||||||
|
balance: {
|
||||||
|
'Yes, very important': 5,
|
||||||
|
'Somewhat important': 3,
|
||||||
|
'Not as important': 1,
|
||||||
|
},
|
||||||
|
recognition: {
|
||||||
|
'Very important': 5,
|
||||||
|
'Somewhat important': 3,
|
||||||
|
'Not as important': 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return weightMap[priority][response] || 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="career-explorer-container bg-white p-6 rounded shadow">
|
||||||
|
{showModal && (
|
||||||
|
<CareerPrioritiesModal
|
||||||
|
userProfile={userProfile}
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-xl font-semibold">Explore Careers</h2>
|
||||||
|
<CareerSearch
|
||||||
|
onCareerSelected={(careerObj) => handleCareerClick(careerObj)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Career Comparison Matrix</h2>
|
||||||
|
{careerList.length ? (
|
||||||
|
<table className="w-full mb-4">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="border p-2">Career</th>
|
||||||
|
{priorityKeys.map((priority) => (
|
||||||
|
<th key={priority} className="border p-2 capitalize">{priority}</th>
|
||||||
|
))}
|
||||||
|
<th className="border p-2">Match</th>
|
||||||
|
<th className="border p-2">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{careerList.map((career) => {
|
||||||
|
const ratings = career.ratings || {};
|
||||||
|
const interestsRating = ratings.interests || 3; // default to 3 if not rated via InterestInventory
|
||||||
|
const meaningRating = ratings.meaning || 3;
|
||||||
|
const stabilityRating = ratings.stability || 3;
|
||||||
|
const growthRating = ratings.growth || 3;
|
||||||
|
const balanceRating = ratings.balance || 3;
|
||||||
|
const recognitionRating = ratings.recognition || 3;
|
||||||
|
|
||||||
|
const userInterestsWeight = priorityWeight('interests', priorities.interests || 'I’m not sure yet');
|
||||||
|
const userMeaningWeight = priorityWeight('meaning', priorities.meaning);
|
||||||
|
const userStabilityWeight = priorityWeight('stability', priorities.stability);
|
||||||
|
const userGrowthWeight = priorityWeight('growth', priorities.growth);
|
||||||
|
const userBalanceWeight = priorityWeight('balance', priorities.balance);
|
||||||
|
const userRecognitionWeight = priorityWeight('recognition', priorities.recognition);
|
||||||
|
|
||||||
|
const totalWeight =
|
||||||
|
userInterestsWeight +
|
||||||
|
userMeaningWeight +
|
||||||
|
userStabilityWeight +
|
||||||
|
userGrowthWeight +
|
||||||
|
userBalanceWeight +
|
||||||
|
userRecognitionWeight;
|
||||||
|
|
||||||
|
const weightedScore =
|
||||||
|
interestsRating * userInterestsWeight +
|
||||||
|
meaningRating * userMeaningWeight +
|
||||||
|
stabilityRating * userStabilityWeight +
|
||||||
|
growthRating * userGrowthWeight +
|
||||||
|
balanceRating * userBalanceWeight +
|
||||||
|
recognitionRating * userRecognitionWeight;
|
||||||
|
|
||||||
|
const matchScore = (weightedScore / (totalWeight * 5)) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={career.code}>
|
||||||
|
<td className="border p-2">{career.title}</td>
|
||||||
|
<td className="border p-2">{interestsRating}</td>
|
||||||
|
<td className="border p-2">{meaningRating}</td>
|
||||||
|
<td className="border p-2">{stabilityRating}</td>
|
||||||
|
<td className="border p-2">{growthRating}</td>
|
||||||
|
<td className="border p-2">{balanceRating}</td>
|
||||||
|
<td className="border p-2">{recognitionRating}</td>
|
||||||
|
<td className="border p-2 font-bold">{matchScore.toFixed(1)}%</td>
|
||||||
|
<td className="border p-2">
|
||||||
|
<button className="text-red-500" onClick={() => removeCareerFromList(career.code)}>Remove</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
) : <p>No careers added to comparison.</p>}
|
||||||
|
|
||||||
|
|
||||||
|
<div className="flex gap-4 mb-4">
|
||||||
|
<select
|
||||||
|
className="border px-3 py-1 rounded"
|
||||||
|
value={selectedJobZone}
|
||||||
|
onChange={(e) => setSelectedJobZone(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">All Preparation Levels</option>
|
||||||
|
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||||
|
<option key={zone} value={zone}>{label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="border px-3 py-1 rounded"
|
||||||
|
value={selectedFit}
|
||||||
|
onChange={(e) => setSelectedFit(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">All Fit Levels</option>
|
||||||
|
{Object.entries(fitLabels).map(([key, label]) => (
|
||||||
|
<option key={key} value={key}>{label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CareerSuggestions
|
||||||
|
careerSuggestions={filteredCareers}
|
||||||
|
onCareerClick={(career) => {
|
||||||
|
setSelectedCareer(career);
|
||||||
|
handleCareerClick(career);
|
||||||
|
}}
|
||||||
|
setLoading={setLoading}
|
||||||
|
setProgress={setProgress}
|
||||||
|
userState={userState}
|
||||||
|
areaTitle={areaTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedCareer && (
|
||||||
|
<CareerModal
|
||||||
|
career={selectedCareer}
|
||||||
|
closeModal={() => setSelectedCareer(null)}
|
||||||
|
userState={userState}
|
||||||
|
areaTitle={areaTitle}
|
||||||
|
userZipcode={userProfile?.zipcode}
|
||||||
|
addCareerToList={addCareerToList} // <-- explicitly added here
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
|
||||||
|
Career results and details provided by
|
||||||
|
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>,
|
||||||
|
in partnership with
|
||||||
|
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer"> Bureau of Labor Statistics</a>
|
||||||
|
and
|
||||||
|
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer"> NCES</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CareerExplorer;
|
219
src/components/CareerModal.js
Normal file
219
src/components/CareerModal.js
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { getFullStateName } from '../utils/stateUtils.js';
|
||||||
|
import { fetchSchools, clientGeocodeZip, haversineDistance} from '../utils/apiUtils.js';
|
||||||
|
|
||||||
|
const apiUrl = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
|
function CareerModal({ career, userState, areaTitle, userZipcode, closeModal, addCareerToList }) {
|
||||||
|
const [careerDetails, setCareerDetails] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCareerClick = async () => {
|
||||||
|
const socCode = career.code;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||||
|
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
||||||
|
const { cipCode } = await cipResponse.json();
|
||||||
|
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||||
|
|
||||||
|
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||||
|
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
||||||
|
const { description, tasks } = await jobDetailsResponse.json();
|
||||||
|
|
||||||
|
const salaryResponse = await axios.get(`${apiUrl}/salary`, {
|
||||||
|
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
||||||
|
}).catch(() => ({ data: {} }));
|
||||||
|
|
||||||
|
const economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
|
||||||
|
params: { state: getFullStateName(userState) },
|
||||||
|
}).catch(() => ({ data: {} }));
|
||||||
|
|
||||||
|
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
||||||
|
|
||||||
|
let userLat = null, userLng = null;
|
||||||
|
if (userZipcode) {
|
||||||
|
try {
|
||||||
|
const geocodeResult = await clientGeocodeZip(userZipcode);
|
||||||
|
userLat = geocodeResult.lat;
|
||||||
|
userLng = geocodeResult.lng;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Unable to geocode user ZIP.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const schoolsWithDistance = filteredSchools.map(sch => {
|
||||||
|
const lat2 = sch.LATITUDE ? parseFloat(sch.LATITUDE) : null;
|
||||||
|
const lon2 = sch.LONGITUD ? parseFloat(sch.LONGITUD) : null;
|
||||||
|
|
||||||
|
if (userLat && userLng && lat2 && lon2) {
|
||||||
|
const distMiles = haversineDistance(userLat, userLng, lat2, lon2);
|
||||||
|
return { ...sch, distance: `${distMiles.toFixed(1)} mi` };
|
||||||
|
}
|
||||||
|
return { ...sch, distance: 'N/A' };
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const sData = salaryResponse.data || {};
|
||||||
|
const salaryDataPoints = sData && Object.keys(sData).length > 0 ? [
|
||||||
|
{ percentile: '10th', regionalSalary: sData.regional?.regional_PCT10 || 0, nationalSalary: sData.national?.national_PCT10 || 0 },
|
||||||
|
{ percentile: '25th', regionalSalary: sData.regional?.regional_PCT25 || 0, nationalSalary: sData.national?.national_PCT25 || 0 },
|
||||||
|
{ percentile: 'Median', regionalSalary: sData.regional?.regional_MEDIAN || 0, nationalSalary: sData.national?.national_MEDIAN || 0 },
|
||||||
|
{ percentile: '75th', regionalSalary: sData.regional?.regional_PCT75 || 0, nationalSalary: sData.national?.national_PCT75 || 0 },
|
||||||
|
{ percentile: '90th', regionalSalary: sData.regional?.regional_PCT90 || 0, nationalSalary: sData.national?.national_PCT90 || 0 },
|
||||||
|
] : [];
|
||||||
|
|
||||||
|
setCareerDetails({
|
||||||
|
...career,
|
||||||
|
jobDescription: description,
|
||||||
|
tasks,
|
||||||
|
economicProjections: economicResponse.data || {},
|
||||||
|
salaryData: salaryDataPoints,
|
||||||
|
schools: schoolsWithDistance,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setError('Failed to load career details.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCareerClick();
|
||||||
|
}, [career, userState, areaTitle, userZipcode]);
|
||||||
|
|
||||||
|
if (loading) return <div>Loading...</div>;
|
||||||
|
if (error) return <div>{error}</div>;
|
||||||
|
|
||||||
|
const calculateStabilityRating = (salaryData) => {
|
||||||
|
const medianSalaryObj = salaryData.find(s => s.percentile === 'Median');
|
||||||
|
const medianSalary = medianSalaryObj?.regionalSalary || medianSalaryObj?.nationalSalary || 0;
|
||||||
|
|
||||||
|
if (medianSalary >= 90000) return 5;
|
||||||
|
if (medianSalary >= 70000) return 4;
|
||||||
|
if (medianSalary >= 50000) return 3;
|
||||||
|
if (medianSalary >= 30000) return 2;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center overflow-auto z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg w-full max-w-5xl p-6 m-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mb-4 pb-2 border-b">
|
||||||
|
<h2 className="text-2xl font-bold text-blue-600">{careerDetails.title}</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const stabilityRating = calculateStabilityRating(careerDetails.salaryData);
|
||||||
|
addCareerToList({
|
||||||
|
...careerDetails,
|
||||||
|
ratings: {
|
||||||
|
stability: stabilityRating
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="text-white bg-green-500 hover:bg-green-600 rounded px-3 py-1"
|
||||||
|
>
|
||||||
|
Add to Comparison
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className="text-white bg-red-500 hover:bg-red-600 rounded px-3 py-1"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Job Description */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">Job Description:</h3>
|
||||||
|
<p className="text-gray-700">{careerDetails.jobDescription}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tasks (full width) */}
|
||||||
|
<div className="mb-4 border-t pt-3">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Tasks:</h3>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
{careerDetails.tasks.map((task, i) => (
|
||||||
|
<li key={i}>{task}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Salary and Economic Projections side-by-side */}
|
||||||
|
<div className="flex flex-col md:flex-row gap-4 border-t pt-3">
|
||||||
|
{/* Salary Data */}
|
||||||
|
<div className="md:w-1/2 overflow-x-auto">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Salary Data:</h3>
|
||||||
|
<table className="w-full text-left border border-gray-300 rounded">
|
||||||
|
<thead className="bg-gray-100">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 border-b">Percentile</th>
|
||||||
|
<th className="px-3 py-2 border-b">Regional Salary</th>
|
||||||
|
<th className="px-3 py-2 border-b">National Salary</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{careerDetails.salaryData.map((s, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="px-3 py-2 border-b">{s.percentile}</td>
|
||||||
|
<td className="px-3 py-2 border-b">${s.regionalSalary.toLocaleString()}</td>
|
||||||
|
<td className="px-3 py-2 border-b">${s.nationalSalary.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Economic Projections */}
|
||||||
|
<div className="md:w-1/2 overflow-x-auto">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Economic Projections:</h3>
|
||||||
|
<table className="w-full text-left border border-gray-300 rounded">
|
||||||
|
<thead className="bg-gray-100">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 border-b">Region</th>
|
||||||
|
<th className="px-3 py-2 border-b">Current Jobs</th>
|
||||||
|
<th className="px-3 py-2 border-b">Jobs in 10 yrs</th>
|
||||||
|
<th className="px-3 py-2 border-b">Growth %</th>
|
||||||
|
<th className="px-3 py-2 border-b">Annual Openings</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{careerDetails.economicProjections.state && (
|
||||||
|
<tr>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.state.area}</td>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.state.base.toLocaleString()}</td>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.state.projection.toLocaleString()}</td>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.state.percentChange}%</td>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{careerDetails.economicProjections.national && (
|
||||||
|
<tr>
|
||||||
|
<td className="px-3 py-2 border-b">National</td>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.national.base.toLocaleString()}</td>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.national.projection.toLocaleString()}</td>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.national.percentChange}%</td>
|
||||||
|
<td className="px-3 py-2 border-b">{careerDetails.economicProjections.national.annualOpenings.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CareerModal;
|
113
src/components/CareerPrioritiesModal.js
Normal file
113
src/components/CareerPrioritiesModal.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import authFetch from '../utils/authFetch.js';
|
||||||
|
|
||||||
|
const CareerPrioritiesModal = ({ userProfile, onClose }) => {
|
||||||
|
const [responses, setResponses] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userProfile?.career_priorities) {
|
||||||
|
setResponses(JSON.parse(userProfile.career_priorities));
|
||||||
|
}
|
||||||
|
}, [userProfile]);
|
||||||
|
|
||||||
|
const questions = [
|
||||||
|
{
|
||||||
|
id: 'interests',
|
||||||
|
text: 'What kinds of activities do you naturally enjoy?',
|
||||||
|
options: ['I know my interests (completed inventory)', 'I’m not sure yet'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meaning',
|
||||||
|
text: 'Is it important your job helps others or makes a difference?',
|
||||||
|
options: ['Yes, very important', 'Somewhat important', 'Not as important'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stability',
|
||||||
|
text: 'How important is it that your career pays well?',
|
||||||
|
options: ['Very important', 'Somewhat important', 'Not as important'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'growth',
|
||||||
|
text: 'Do you want clear chances to advance and grow professionally?',
|
||||||
|
options: ['Yes, very important', 'Somewhat important', 'Not as important'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'balance',
|
||||||
|
text: 'Do you prefer a job with flexible hours and time outside work?',
|
||||||
|
options: ['Yes, very important', 'Somewhat important', 'Not as important'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recognition',
|
||||||
|
text: 'How important is it to have a career that others admire?',
|
||||||
|
options: ['Very important', 'Somewhat important', 'Not as important'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const payload = {
|
||||||
|
firstName: userProfile.firstname,
|
||||||
|
lastName: userProfile.lastname,
|
||||||
|
email: userProfile.email,
|
||||||
|
zipCode: userProfile.zipcode,
|
||||||
|
state: userProfile.state,
|
||||||
|
area: userProfile.area,
|
||||||
|
careerSituation: userProfile.career_situation || null,
|
||||||
|
career_priorities: JSON.stringify(responses),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authFetch('/api/user-profile', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving priorities:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const allAnswered = questions.every(q => responses[q.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-60 flex justify-center items-center">
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-lg w-full max-w-2xl overflow-y-auto max-h-[90vh]">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Tell us what's important to you</h2>
|
||||||
|
{questions.map(q => (
|
||||||
|
<div key={q.id} className="mb-4">
|
||||||
|
<label className="block mb-2 font-medium">{q.text}</label>
|
||||||
|
<select
|
||||||
|
value={responses[q.id] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setResponses({ ...responses, [q.id]: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full border px-3 py-2 rounded"
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
Select an answer
|
||||||
|
</option>
|
||||||
|
{q.options.map((opt) => (
|
||||||
|
<option key={opt} value={opt}>
|
||||||
|
{opt}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!allAnswered}
|
||||||
|
className={`px-4 py-2 rounded ${allAnswered ? 'bg-blue-600 text-white' : 'bg-gray-300 cursor-not-allowed'}`}
|
||||||
|
>
|
||||||
|
Save Answers
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CareerPrioritiesModal;
|
@ -20,7 +20,7 @@ export function CareerSuggestions({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
const checkCareerDataAvailability = async () => {
|
const checkCareerDataAvailability = async () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { fetchSchools } from '../utils/apiUtils.ja';
|
import { fetchSchools } from '../utils/apiUtils.js';
|
||||||
|
|
||||||
function EducationalPrograms({ cipCode, userState }) {
|
function EducationalPrograms({ cipCode, userState }) {
|
||||||
const [schools, setSchools] = useState([]);
|
const [schools, setSchools] = useState([]);
|
||||||
|
@ -156,7 +156,7 @@ const InterestInventory = () => {
|
|||||||
const { careers: careerSuggestions, riaSecScores } = data;
|
const { careers: careerSuggestions, riaSecScores } = data;
|
||||||
|
|
||||||
if (Array.isArray(careerSuggestions) && Array.isArray(riaSecScores)) {
|
if (Array.isArray(careerSuggestions) && Array.isArray(riaSecScores)) {
|
||||||
navigate('/dashboard', { state: { careerSuggestions, riaSecScores } });
|
navigate('/career-explorer', { state: { careerSuggestions, riaSecScores } });
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid data format from the server.');
|
throw new Error('Invalid data format from the server.');
|
||||||
}
|
}
|
||||||
|
@ -13,31 +13,39 @@ function PlanningLanding() {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 mb-6 text-center">
|
<p className="text-gray-600 mb-6 text-center">
|
||||||
Discover career options that match your interests, skills, and potential.
|
Discover career options that match your interests, skills, and potential.
|
||||||
AptivaAI helps you find your ideal career path, provides insights into the educational requirements,
|
AptivaAI helps you find your ideal career path, provides insights into educational requirements,
|
||||||
expected salaries, job market trends, and more.
|
expected salaries, job market trends, and more.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-6">
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => navigate('/interest-inventory')}
|
|
||||||
>
|
|
||||||
Take Interest Inventory
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div>
|
||||||
className="w-full"
|
<Button className="w-full" onClick={() => navigate('/interest-inventory')}>
|
||||||
onClick={() => navigate('/career-explorer')}
|
Take Interest Inventory
|
||||||
>
|
</Button>
|
||||||
Explore Career Paths
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
</Button>
|
Identify your interests and discover careers aligned with your strengths.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button className="w-full" onClick={() => navigate('/career-explorer')}>
|
||||||
|
Explore Career Paths
|
||||||
|
</Button>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
|
Research detailed career profiles, job descriptions, salaries, and employment outlooks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button className="w-full" onClick={() => navigate('/educational-programs')}>
|
||||||
|
Discover Educational Programs
|
||||||
|
</Button>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
|
Find the right educational programs, degrees, and certifications needed to pursue your chosen career.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => navigate('/educational-programs')}
|
|
||||||
>
|
|
||||||
Discover Educational Programs
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
75
src/utils/PopulateCareerRatings.js
Normal file
75
src/utils/PopulateCareerRatings.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
|
const careersFile = '../../updated_career_data_final.json';
|
||||||
|
const outputFile = '../../careers_with_ratings.json';
|
||||||
|
|
||||||
|
const onetUsername = 'aptivaai';
|
||||||
|
const onetPassword = '2296ahq';
|
||||||
|
|
||||||
|
// Sleep function for delay between calls
|
||||||
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
// Helper function to convert O*Net numeric scores (0-100) to a 1-5 scale
|
||||||
|
const mapScoreToScale = (score) => {
|
||||||
|
if(score >= 80) return 5;
|
||||||
|
if(score >= 60) return 4;
|
||||||
|
if(score >= 40) return 3;
|
||||||
|
if(score >= 20) return 2;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fully corrected function to fetch ratings from O*Net API
|
||||||
|
const fetchCareerRatingsCorrected = async (socCode) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`https://services.onetcenter.org/ws/online/occupations/${socCode}/details`,
|
||||||
|
{ auth: { username: onetUsername, password: onetPassword } }
|
||||||
|
);
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
// Correctly parse the work_values array from O*Net's structure
|
||||||
|
const workValues = Array.isArray(data.work_values?.element) ? data.work_values.element : [];
|
||||||
|
|
||||||
|
// Correctly parse the bright_outlook boolean from O*Net's structure
|
||||||
|
const brightOutlook = data.occupation.tags.bright_outlook === true;
|
||||||
|
|
||||||
|
// Extract numerical scores for balance and recognition
|
||||||
|
const balanceValue = workValues.find(v => v.name === 'Working Conditions')?.score.value || 0;
|
||||||
|
const recognitionValue = workValues.find(v => v.name === 'Recognition')?.score.value || 0;
|
||||||
|
|
||||||
|
// Return mapped ratings accurately reflecting O*Net data
|
||||||
|
return {
|
||||||
|
growth: brightOutlook ? 5 : 1,
|
||||||
|
balance: mapScoreToScale(balanceValue),
|
||||||
|
recognition: mapScoreToScale(recognitionValue)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching details for ${socCode}:`, error.message);
|
||||||
|
return {
|
||||||
|
growth: 1,
|
||||||
|
balance: 1,
|
||||||
|
recognition: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main function to populate ratings for all careers
|
||||||
|
const populateRatingsCorrected = async () => {
|
||||||
|
const careersData = JSON.parse(await fs.readFile(careersFile, 'utf8'));
|
||||||
|
for (let career of careersData) {
|
||||||
|
const ratings = await fetchCareerRatingsCorrected(career.soc_code);
|
||||||
|
if (ratings) {
|
||||||
|
career.ratings = ratings;
|
||||||
|
console.log(`Fetched ratings for: ${career.title}`);
|
||||||
|
}
|
||||||
|
// Wait for .5 seconds between API calls to avoid hitting API limits
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(outputFile, JSON.stringify(careersData, null, 2));
|
||||||
|
console.log(`Ratings populated to: ${outputFile}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the corrected function to populate ratings
|
||||||
|
populateRatingsCorrected();
|
@ -17,6 +17,35 @@ export const fetchAreasByState = async (state) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function clientGeocodeZip(zip) {
|
||||||
|
const apiKey = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
|
||||||
|
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(zip)}&key=${apiKey}`;
|
||||||
|
|
||||||
|
const resp = await axios.get(url);
|
||||||
|
if (resp.data.status === 'OK' && resp.data.results && resp.data.results.length > 0) {
|
||||||
|
return resp.data.results[0].geometry.location; // { lat, lng }
|
||||||
|
}
|
||||||
|
throw new Error('Geocoding failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// utils/apiUtils.js
|
||||||
|
|
||||||
|
export function haversineDistance(lat1, lon1, lat2, lon2) {
|
||||||
|
const R = 3959; // radius of earth in miles
|
||||||
|
const toRad = (val) => (val * Math.PI) / 180;
|
||||||
|
|
||||||
|
const dLat = toRad(lat2 - lat1);
|
||||||
|
const dLon = toRad(lon2 - lon1);
|
||||||
|
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat / 2) ** 2 +
|
||||||
|
Math.cos(toRad(lat1)) *
|
||||||
|
Math.cos(toRad(lat2)) *
|
||||||
|
Math.sin(dLon / 2) ** 2;
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch schools
|
// Fetch schools
|
||||||
export const fetchSchools = async (cipCode, state = '', level = '', type = '') => {
|
export const fetchSchools = async (cipCode, state = '', level = '', type = '') => {
|
||||||
|
158
src/utils/handleCareerClick.js
Normal file
158
src/utils/handleCareerClick.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// ============= handleCareerClick =============
|
||||||
|
const handleCareerClick = useCallback(
|
||||||
|
async (career) => {
|
||||||
|
console.log('[handleCareerClick] career =>', career);
|
||||||
|
const socCode = career.code;
|
||||||
|
console.log('[handleCareerClick] career.code =>', socCode);
|
||||||
|
setSelectedCareer(career);
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setCareerDetails({});
|
||||||
|
setSchools([]);
|
||||||
|
setSalaryData([]);
|
||||||
|
setEconomicProjections({});
|
||||||
|
setTuitionData([]);
|
||||||
|
|
||||||
|
if (!socCode) {
|
||||||
|
console.error('SOC Code is missing');
|
||||||
|
setError('SOC Code is missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// CIP fetch
|
||||||
|
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||||
|
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
||||||
|
const { cipCode } = await cipResponse.json();
|
||||||
|
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||||
|
|
||||||
|
// Job details
|
||||||
|
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||||
|
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
||||||
|
const { description, tasks } = await jobDetailsResponse.json();
|
||||||
|
|
||||||
|
// Salary
|
||||||
|
let salaryResponse;
|
||||||
|
try {
|
||||||
|
salaryResponse = await axios.get(`${apiUrl}/salary`, {
|
||||||
|
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
salaryResponse = { data: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullName = getFullStateName(userState);
|
||||||
|
|
||||||
|
// Economic
|
||||||
|
let economicResponse;
|
||||||
|
try {
|
||||||
|
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
|
||||||
|
params: { state: fullName }, // e.g. "Kentucky"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
economicResponse = { data: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tuition
|
||||||
|
let tuitionResponse;
|
||||||
|
try {
|
||||||
|
tuitionResponse = await axios.get(`${apiUrl}/tuition`, {
|
||||||
|
params: { cipCode: cleanedCipCode, state: userState },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
tuitionResponse = { data: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** FETCH SCHOOLS NORMALLY **
|
||||||
|
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
||||||
|
|
||||||
|
// ** 1) Geocode user zip once on the client **
|
||||||
|
let userLat = null;
|
||||||
|
let userLng = null;
|
||||||
|
if (userZipcode) {
|
||||||
|
try {
|
||||||
|
const geocodeResult = await clientGeocodeZip(userZipcode);
|
||||||
|
userLat = geocodeResult.lat;
|
||||||
|
userLng = geocodeResult.lng;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Unable to geocode user ZIP, distances will be N/A.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** 2) Compute Haversine distance locally for each school **
|
||||||
|
const schoolsWithDistance = filteredSchools.map((sch) => {
|
||||||
|
// only if we have lat/lon for both user + school
|
||||||
|
const lat2 = sch.LATITUDE ? parseFloat(sch.LATITUDE) : null;
|
||||||
|
const lon2 = sch.LONGITUD ? parseFloat(sch.LONGITUD) : null;
|
||||||
|
|
||||||
|
if (userLat && userLng && lat2 && lon2) {
|
||||||
|
const distMiles = haversineDistance(userLat, userLng, lat2, lon2);
|
||||||
|
return {
|
||||||
|
...sch,
|
||||||
|
distance: distMiles.toFixed(1) + ' mi',
|
||||||
|
duration: 'N/A',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...sch,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Build final details
|
||||||
|
const updatedCareerDetails = {
|
||||||
|
...career,
|
||||||
|
jobDescription: description,
|
||||||
|
tasks: tasks,
|
||||||
|
economicProjections: economicResponse.data || {},
|
||||||
|
salaryData: salaryDataPoints,
|
||||||
|
schools: schoolsWithDistance,
|
||||||
|
tuitionData: tuitionResponse.data || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
setCareerDetails(updatedCareerDetails);
|
||||||
|
updateChatbotContext({ careerDetails: updatedCareerDetails });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing career click:', error.message);
|
||||||
|
setError('Failed to load data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[userState, apiUrl, areaTitle, userZipcode, updateChatbotContext]
|
||||||
|
);
|
35
src/utils/stateUtils.js
Normal file
35
src/utils/stateUtils.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// src/utils/stateUtils.js
|
||||||
|
|
||||||
|
export const STATES = [
|
||||||
|
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' },
|
||||||
|
{ name: 'Arizona', code: 'AZ' }, { name: 'Arkansas', code: 'AR' },
|
||||||
|
{ 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' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getFullStateName(code) {
|
||||||
|
const found = STATES.find((s) => s.code === code?.toUpperCase());
|
||||||
|
return found ? found.name : '';
|
||||||
|
}
|
71
src/utils/testOutputFile.js
Normal file
71
src/utils/testOutputFile.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
|
const testOutputFile = '../../test_careers_with_ratings.json';
|
||||||
|
|
||||||
|
const onetUsername = 'aptivaai';
|
||||||
|
const onetPassword = '2296ahq';
|
||||||
|
|
||||||
|
const testSocCodes = [
|
||||||
|
'19-4071.00',
|
||||||
|
'15-1241.00',
|
||||||
|
];
|
||||||
|
|
||||||
|
const mapScoreToScale = (score) => {
|
||||||
|
if(score >= 80) return 5;
|
||||||
|
if(score >= 60) return 4;
|
||||||
|
if(score >= 40) return 3;
|
||||||
|
if(score >= 20) return 2;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchCareerRatingsTest = async (socCode) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`https://services.onetcenter.org/ws/online/occupations/${socCode}/details`,
|
||||||
|
{ auth: { username: onetUsername, password: onetPassword } }
|
||||||
|
);
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
const workValues = Array.isArray(data.work_values?.element) ? data.work_values.element : [];
|
||||||
|
|
||||||
|
const brightOutlook = data.occupation.tags.bright_outlook === true;
|
||||||
|
|
||||||
|
const balanceValue = workValues.find(v => v.name === 'Working Conditions')?.score.value || 0;
|
||||||
|
const recognitionValue = workValues.find(v => v.name === 'Recognition')?.score.value || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
soc_code: socCode,
|
||||||
|
title: data.occupation.title,
|
||||||
|
ratings: {
|
||||||
|
growth: brightOutlook ? 5 : 1,
|
||||||
|
balance: mapScoreToScale(balanceValue),
|
||||||
|
recognition: mapScoreToScale(recognitionValue)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching details for ${socCode}:`, error.message);
|
||||||
|
return {
|
||||||
|
soc_code,
|
||||||
|
title: "Fetch Error",
|
||||||
|
ratings: { growth: 1, balance: 1, recognition: 1 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runQuickTest = async () => {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const socCode of testSocCodes) {
|
||||||
|
const careerData = await fetchCareerRatingsTest(socCode);
|
||||||
|
if (careerData) {
|
||||||
|
results.push(careerData);
|
||||||
|
console.log(`Fetched ratings for: ${careerData.title}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(testOutputFile, JSON.stringify(results, null, 2));
|
||||||
|
console.log(`Test ratings populated to: ${testOutputFile}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
runQuickTest();
|
20
test_careers_with_ratings.json
Normal file
20
test_careers_with_ratings.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"soc_code": "19-4071.00",
|
||||||
|
"title": "Forest and Conservation Technicians",
|
||||||
|
"ratings": {
|
||||||
|
"growth": 1,
|
||||||
|
"balance": 3,
|
||||||
|
"recognition": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"soc_code": "15-1241.00",
|
||||||
|
"title": "Computer Network Architects",
|
||||||
|
"ratings": {
|
||||||
|
"growth": 5,
|
||||||
|
"balance": 4,
|
||||||
|
"recognition": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
32531
updated_career_data_final.json
Normal file
32531
updated_career_data_final.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user