Added Skills to EducationalProgramsPage.js and pass Career from CareerExplorer.js

This commit is contained in:
Josh 2025-05-19 12:08:55 +00:00
parent 3b4c44c088
commit 72e52d57fe
5 changed files with 372 additions and 0 deletions

View File

@ -639,6 +639,89 @@ app.post('/api/job-zones', async (req, res) => {
} }
}); });
/**************************************************
* O*NET Skills route
**************************************************/
app.get('/api/skills/:socCode', async (req, res) => {
const { socCode } = req.params;
if (!socCode) {
return res.status(400).json({ error: 'SOC code is required' });
}
console.log(`Fetching O*NET skills for SOC Code: ${socCode}`);
try {
// 1) Build the O*NET API URL
const onetUrl = `https://services.onetcenter.org/ws/mnm/careers/${socCode}/skills`;
// 2) Call O*NET with Basic Auth
const response = await axios.get(onetUrl, {
auth: {
username: process.env.ONET_USERNAME,
password: process.env.ONET_PASSWORD
},
headers: { Accept: 'application/json' }
});
const data = response.data || {};
// 3) O*NET returns:
// {
// "code": "17-1011.00",
// "group": [
// {
// "title": { "id": "2.A", "name": "Basic Skills" },
// "element": [
// { "id": "2.A.1.a", "name": "reading work related information" },
// ...
// ]
// },
// ...
// ]
// }
// Instead of data.characteristic, parse data.group
const groups = data.group || [];
// 4) Flatten out the group[].element[] into a single skills array
const skills = [];
groups.forEach((groupItem) => {
const groupName = groupItem?.title?.name || 'Unknown Group';
if (Array.isArray(groupItem.element)) {
groupItem.element.forEach((elem) => {
// Each "element" is a skill with an id and a name
skills.push({
groupName,
skillId: elem.id,
skillName: elem.name
});
});
}
});
res.json({ skills });
} catch (error) {
console.error('Error fetching O*NET skills:', error.message);
if (error.response) {
console.error('O*NET error status:', error.response.status);
console.error('O*NET error data:', error.response.data);
} else if (error.request) {
// The request was made but no response was received
console.error('No response received from O*NET. Possibly a network or credentials error.');
console.error('Axios error.request:', error.request);
} else {
// Something else happened in setting up the request
console.error('Request setup error:', error.message);
}
return res.status(500).json({ error: 'Failed to fetch O*NET skills' });
}
});
/************************************************** /**************************************************
* user-profile by ID route * user-profile by ID route
**************************************************/ **************************************************/

View File

@ -456,6 +456,7 @@ function CareerExplorer() {
// 4) Navigate // 4) Navigate
navigate('/educational-programs', { navigate('/educational-programs', {
state: { state: {
socCode: career.code,
cipCodes: cleanedCips, cipCodes: cleanedCips,
careerTitle: career.title, careerTitle: career.title,
userZip: userZipcode, userZip: userZipcode,

View File

@ -7,12 +7,17 @@ function EducationalProgramsPage() {
// 1) Read an array of CIP codes from route state (if provided), // 1) Read an array of CIP codes from route state (if provided),
// or default to an empty array. // or default to an empty array.
const location = useLocation(); const location = useLocation();
const [socCode, setsocCode] = useState(location.state?.socCode || '');
const [cipCodes, setCipCodes] = useState(location.state?.cipCodes || []); const [cipCodes, setCipCodes] = useState(location.state?.cipCodes || []);
// userState / userZip from route state or user profile // userState / userZip from route state or user profile
const [userState, setUserState] = useState(location.state?.userState || ''); const [userState, setUserState] = useState(location.state?.userState || '');
const [userZip, setUserZip] = useState(location.state?.userZip || ''); const [userZip, setUserZip] = useState(location.state?.userZip || '');
const [skills, setSkills] = useState([]); // <== new
const [loadingSkills, setLoadingSkills] = useState(false);
const [skillsError, setSkillsError] = useState(null);
// For UI // For UI
const [schools, setSchools] = useState([]); const [schools, setSchools] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -74,6 +79,32 @@ useEffect(() => {
loadUserProfile(); loadUserProfile();
}, []); }, []);
useEffect(() => {
if (!socCode) return; // no soc => skip
async function loadSkills() {
setLoadingSkills(true);
setSkillsError(null);
try {
// Example: GET /api/skills/15-1241
const response = await fetch(`/api/skills/${socCode}`);
if (!response.ok) {
throw new Error('Failed to fetch skills');
}
const data = await response.json();
// Suppose data = { skills: [ { name: 'Active Listening', importance: 72 }, ... ] }
setSkills(data.skills || []);
} catch (err) {
console.error('Error fetching skills:', err);
setSkillsError('Failed to load skills');
} finally {
setLoadingSkills(false);
}
}
loadSkills();
}, [socCode]);
// ============== Fetch schools once we have CIP codes ============== // ============== Fetch schools once we have CIP codes ==============
useEffect(() => { useEffect(() => {
if (!cipCodes.length) return; // no CIP codes => show CareerSearch fallback if (!cipCodes.length) return; // no CIP codes => show CareerSearch fallback
@ -206,8 +237,49 @@ useEffect(() => {
); );
} }
const skillSection = () => {
if (loadingSkills) {
return <p>Loading required skills for this career...</p>;
}
if (skillsError) {
return <p className="text-red-600">{skillsError}</p>;
}
if (!skills.length) {
return <p>No specific skill data found for this career.</p>;
}
// Each skill now looks like: { groupName, skillId, skillName }
return (
<div>
<h3 className="text-xl font-semibold mb-2">Skills Required</h3>
{/* Option 1: Flat table of skillName only */}
<table className="w-full border mb-4">
<thead>
<tr>
<th className="border p-2">Group</th>
<th className="border p-2">Skill</th>
</tr>
</thead>
<tbody>
{skills.map((sk, idx) => (
<tr key={idx}>
<td className="border p-2">{sk.groupName}</td>
<td className="border p-2">{sk.skillName}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
return ( return (
<div className="p-4"> <div className="p-4">
{skillSection()}
<h2>Schools for: {careerTitle || 'Unknown Career'}</h2> <h2>Schools for: {careerTitle || 'Unknown Career'}</h2>

Binary file not shown.

216
user_profile.sql Normal file

File diff suppressed because one or more lines are too long