Fixed limited data behavior, All Others in CareerExplorer
This commit is contained in:
parent
b5184f2a02
commit
13e898242c
2
.env
2
.env
@ -2,4 +2,4 @@ CORS_ALLOWED_ORIGINS=https://dev1.aptivaai.com,http://34.16.120.118:3000,http://
|
||||
SERVER1_PORT=5000
|
||||
SERVER2_PORT=5001
|
||||
SERVER3_PORT=5002
|
||||
IMG_TAG=8449dc2-202508050250
|
||||
IMG_TAG=6a57e00-202508051419
|
@ -9,6 +9,7 @@ import InterestMeaningModal from './InterestMeaningModal.js';
|
||||
import CareerSearch from './CareerSearch.js';
|
||||
import { Button } from './ui/button.js';
|
||||
import axios from 'axios';
|
||||
import isAllOther from '../utils/isAllOther.js';
|
||||
|
||||
const STATES = [
|
||||
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
|
||||
@ -338,181 +339,103 @@ function CareerExplorer() {
|
||||
}
|
||||
}, [location.state, userProfile, fetchSuggestions, navigate]);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// handleCareerClick (detail fetch for CIP, Salary, etc.)
|
||||
// ------------------------------------------------------
|
||||
const handleCareerClick = useCallback(
|
||||
/* ------------------------------------------------------
|
||||
handleCareerClick – fetches all details for one career
|
||||
---------------------------------------------------- */
|
||||
const handleCareerClick = useCallback(
|
||||
async (career) => {
|
||||
console.log('[handleCareerClick] career object:', JSON.stringify(career, null, 2));
|
||||
|
||||
const socCode = career.code;
|
||||
if (!socCode) return;
|
||||
|
||||
setSelectedCareer(career);
|
||||
setError(null);
|
||||
setCareerDetails(null);
|
||||
setSalaryData([]);
|
||||
setEconomicProjections({});
|
||||
setCareerDetails(null); // reset any previous modal
|
||||
setLoading(true);
|
||||
|
||||
if (!socCode) {
|
||||
console.error('SOC Code is missing');
|
||||
setError('SOC Code is missing');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1) CIP fetch
|
||||
const cipResponse = await fetch(`/api/cip/${socCode}`);
|
||||
if (!cipResponse.ok) {
|
||||
setError(
|
||||
`We're sorry, but specific details for "${career.title}" are not available at this time.`
|
||||
);
|
||||
setCareerDetails({
|
||||
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
/* ---------- 1. CIP lookup ---------- */
|
||||
let cipCode = null;
|
||||
const cipRes = await fetch(`/api/cip/${socCode}`);
|
||||
if (cipRes.ok) {
|
||||
cipCode = (await cipRes.json()).cipCode ?? null;
|
||||
}
|
||||
const { cipCode } = await cipResponse.json();
|
||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||
|
||||
// 2) Job details (description + tasks)
|
||||
const jobDetailsResponse = await fetch(
|
||||
`/api/onet/career-description/${socCode}`
|
||||
);
|
||||
if (!jobDetailsResponse.ok) {
|
||||
setCareerDetails({
|
||||
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
/* ---------- 2. Job description & tasks ---------- */
|
||||
let description = '';
|
||||
let tasks = [];
|
||||
const jobRes = await fetch(`/api/onet/career-description/${socCode}`);
|
||||
if (jobRes.ok) {
|
||||
const jd = await jobRes.json();
|
||||
description = jd.description ?? '';
|
||||
tasks = jd.tasks ?? [];
|
||||
}
|
||||
const { description, tasks } = await jobDetailsResponse.json();
|
||||
|
||||
// 3) Salary data
|
||||
let salaryResponse;
|
||||
try {
|
||||
salaryResponse = await axios.get('/api/salary', {
|
||||
/* ---------- 3. Salary data ---------- */
|
||||
const salaryRes = await axios.get('/api/salary', {
|
||||
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
||||
});
|
||||
} catch (error) {
|
||||
salaryResponse = { data: {} };
|
||||
}
|
||||
}).catch(() => ({ data: {} }));
|
||||
|
||||
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 s = salaryRes.data;
|
||||
const salaryDataPoints = s && Object.keys(s).length > 0 ? [
|
||||
{ percentile: '10th Percentile', regionalSalary: +s.regional?.regional_PCT10 || 0, nationalSalary: +s.national?.national_PCT10 || 0 },
|
||||
{ percentile: '25th Percentile', regionalSalary: +s.regional?.regional_PCT25 || 0, nationalSalary: +s.national?.national_PCT25 || 0 },
|
||||
{ percentile: 'Median', regionalSalary: +s.regional?.regional_MEDIAN || 0, nationalSalary: +s.national?.national_MEDIAN || 0 },
|
||||
{ percentile: '75th Percentile', regionalSalary: +s.regional?.regional_PCT75 || 0, nationalSalary: +s.national?.national_PCT75 || 0 },
|
||||
{ percentile: '90th Percentile', regionalSalary: +s.regional?.regional_PCT90 || 0, nationalSalary: +s.national?.national_PCT90 || 0 },
|
||||
] : [];
|
||||
|
||||
// 4) Economic Projections
|
||||
const fullStateName = getFullStateName(userState); // your helper
|
||||
let economicResponse = { data: {} };
|
||||
try {
|
||||
economicResponse = await axios.get(
|
||||
/* ---------- 4. Economic projections ---------- */
|
||||
const fullStateName = getFullStateName(userState);
|
||||
const projRes = await axios.get(
|
||||
`/api/projections/${socCode.split('.')[0]}`,
|
||||
{
|
||||
params: { state: fullStateName },
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
economicResponse = { data: {} };
|
||||
{ params: { state: fullStateName } }
|
||||
).catch(() => ({ data: {} }));
|
||||
|
||||
/* ---------- 5. Decide if we actually have data ---------- */
|
||||
const haveSalary = salaryDataPoints.length > 0;
|
||||
const haveProj = !!(projRes.data.state || projRes.data.national);
|
||||
const haveJobInfo = description || tasks.length > 0;
|
||||
|
||||
if (!haveSalary && !haveProj && !haveJobInfo) {
|
||||
setCareerDetails({
|
||||
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
||||
});
|
||||
return; // stops here – nothing useful to show
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 5) AI RISK ANALYSIS LOGIC
|
||||
// Attempt to retrieve from server2 first;
|
||||
// if not found => call server3 => store in server2.
|
||||
// ----------------------------------------------------
|
||||
/* ---------- 6. AI‑risk (only if job details exist) ---------- */
|
||||
let aiRisk = null;
|
||||
const strippedSocCode = socCode.split('.')[0];
|
||||
|
||||
|
||||
if (haveJobInfo) {
|
||||
try {
|
||||
// Check local DB first (SQLite -> server2)
|
||||
const localRiskRes = await axios.get(`/api/ai-risk/${socCode}`);
|
||||
aiRisk = localRiskRes.data;
|
||||
aiRisk = (await axios.get(`/api/ai-risk/${socCode}`)).data;
|
||||
} catch (err) {
|
||||
// If 404, we call server3's ChatGPT route at the SAME base url
|
||||
if (err.response && err.response.status === 404) {
|
||||
try {
|
||||
if (err.response?.status === 404) {
|
||||
const aiRes = await axios.post('/api/public/ai-risk-analysis', {
|
||||
socCode,
|
||||
careerName: career.title,
|
||||
jobDescription: description,
|
||||
careerName : career.title,
|
||||
jobDescription : description,
|
||||
tasks,
|
||||
});
|
||||
|
||||
const { riskLevel, reasoning } = aiRes.data;
|
||||
|
||||
// store it back in server2 to avoid repeated GPT calls
|
||||
await axios.post('/api/ai-risk', {
|
||||
socCode,
|
||||
careerName: aiRes.data.careerName,
|
||||
jobDescription: aiRes.data.jobDescription,
|
||||
tasks: aiRes.data.tasks,
|
||||
riskLevel: aiRes.data.riskLevel,
|
||||
reasoning: aiRes.data.reasoning,
|
||||
});
|
||||
|
||||
// build final object
|
||||
aiRisk = {
|
||||
socCode,
|
||||
careerName: career.title,
|
||||
jobDescription: description,
|
||||
tasks,
|
||||
riskLevel,
|
||||
reasoning,
|
||||
};
|
||||
} catch (err2) {
|
||||
console.error('Error calling server3 or storing AI risk:', err2);
|
||||
// fallback
|
||||
aiRisk = aiRes.data;
|
||||
// store for next time (best‑effort)
|
||||
axios.post('/api/ai-risk', aiRisk).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
console.error('Error fetching AI risk from server2:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 6) Build final details object
|
||||
/* ---------- 7. Build the final object & show modal ---------- */
|
||||
const updatedCareerDetails = {
|
||||
...career,
|
||||
cipCode, // may be null – EducationalPrograms handles it
|
||||
jobDescription: description,
|
||||
tasks,
|
||||
salaryData: salaryDataPoints,
|
||||
economicProjections: economicResponse.data || {},
|
||||
aiRisk, // <--- Now we have it attached
|
||||
economicProjections: projRes.data ?? {},
|
||||
aiRisk,
|
||||
};
|
||||
|
||||
setCareerDetails(updatedCareerDetails);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing career click:', error.message);
|
||||
} catch (e) {
|
||||
console.error('[handleCareerClick] fatal:', e);
|
||||
setCareerDetails({
|
||||
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
||||
});
|
||||
@ -520,9 +443,10 @@ function CareerExplorer() {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[userState, areaTitle]
|
||||
[areaTitle, userState]
|
||||
);
|
||||
|
||||
|
||||
// ------------------------------------------------------
|
||||
// handleCareerFromSearch
|
||||
// ------------------------------------------------------
|
||||
@ -1071,7 +995,7 @@ const handleSelectForEducation = (career) => {
|
||||
{/* Legend container with less internal gap, plus a left margin */}
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
<span className="warning-icon">⚠️</span>
|
||||
<span>= Limited Data for this career path</span>
|
||||
<span>= May have limited data for this career path</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -11,23 +11,36 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
const aiRisk = careerDetails?.aiRisk || null;
|
||||
|
||||
|
||||
console.log('CareerModal props:', { career, careerDetails, aiRisk });
|
||||
|
||||
// Handle your normal careerDetails loading logic
|
||||
if (careerDetails?.error) {
|
||||
if (!careerDetails) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<p className="text-lg text-gray-700 mb-4">{careerDetails.error}</p>
|
||||
<button onClick={closeModal} className="bg-red-500 text-white p-2 rounded">
|
||||
Close
|
||||
</button>
|
||||
<p className="text-lg text-gray-700">Loading career details…</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!careerDetails?.salaryData) {
|
||||
// Handle your normal careerDetails loading logic
|
||||
if (careerDetails?.error) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 max-w-xl">
|
||||
<p className="text-lg text-gray-700 mb-4">
|
||||
{careerDetails.error}
|
||||
</p>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="bg-red-500 text-white p-2 rounded"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!careerDetails?.salaryData === undefined) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
@ -65,7 +78,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
You've selected an "umbrella" field that covers a wide range of careers—many
|
||||
people begin a career journey with a broad interest area and we don't want to discourage
|
||||
anyone from taking this approach. It's just difficult to display detailed career data
|
||||
and day‑to‑day tasks for this “all‑other” occupatio.. Use it as a starting point,
|
||||
and day‑to‑day tasks for this “all‑other” occupation. Use it as a starting point,
|
||||
keep exploring specializations, and we can show you richer insights as soon as you are able
|
||||
to narrow it down to a more specific role. If you know this is the field for you, go ahead to
|
||||
add it to your comparison list or move straight into Preparing & Upskilling for Your Career!
|
||||
@ -81,18 +94,20 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
</h2>
|
||||
|
||||
{/* AI RISK SECTION */}
|
||||
{loadingRisk ? (
|
||||
<p className="text-sm text-gray-500 mt-1">Loading AI risk...</p>
|
||||
) : aiRisk?.riskLevel && aiRisk?.reasoning ? (
|
||||
{loadingRisk && (
|
||||
<p className="text-sm text-gray-500 mt-1">Loading AI risk…</p>
|
||||
)}
|
||||
|
||||
{!loadingRisk && aiRisk && aiRisk.riskLevel && aiRisk.reasoning && (
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<strong>AI Risk Level:</strong> {aiRisk.riskLevel}
|
||||
<br />
|
||||
<span>{aiRisk.reasoning}</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
No AI risk data available
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!loadingRisk && !aiRisk && (
|
||||
<p className="text-sm text-gray-500 mt-1">No AI risk data available</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -126,12 +141,15 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
</div>
|
||||
|
||||
{/* Job Description */}
|
||||
{careerDetails.jobDescription && (
|
||||
<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 */}
|
||||
{careerDetails.tasks?.length > 0 && (
|
||||
<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">
|
||||
@ -140,12 +158,22 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{(careerDetails.salaryData?.length > 0 ||
|
||||
(careerDetails.economicProjections &&
|
||||
(careerDetails.economicProjections.state ||
|
||||
careerDetails.economicProjections.national))) && (
|
||||
|
||||
{/* Salary & Projections side-by-side */}
|
||||
<div className="flex flex-col md:flex-row gap-4 border-t pt-3">
|
||||
{/* Salary Data */}
|
||||
|
||||
|
||||
|
||||
{/* ── Salary table ───────────────────────── */}
|
||||
{careerDetails.salaryData?.length > 0 && (
|
||||
<div className="md:w-1/2 overflow-x-auto">
|
||||
<h3 className="text-lg font-semibold mb-2">Salary Data:</h3>
|
||||
<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>
|
||||
@ -155,22 +183,25 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{careerDetails.salaryData.map((s, i) => (
|
||||
{careerDetails.salaryData.map((row, i) => (
|
||||
<tr key={i}>
|
||||
<td className="px-3 py-2 border-b">{s.percentile}</td>
|
||||
<td className="px-3 py-2 border-b">{row.percentile}</td>
|
||||
<td className="px-3 py-2 border-b">
|
||||
${s.regionalSalary.toLocaleString()}
|
||||
${row.regionalSalary.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-3 py-2 border-b">
|
||||
${s.nationalSalary.toLocaleString()}
|
||||
${row.nationalSalary.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Economic Projections */}
|
||||
{/* ── Economic projections ───────────────── */}
|
||||
{(careerDetails.economicProjections?.state ||
|
||||
careerDetails.economicProjections?.national) && (
|
||||
<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">
|
||||
@ -189,7 +220,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Current Jobs</td>
|
||||
<td className="px-3 py-2 border-b font-semibold">
|
||||
Current Jobs
|
||||
</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.base.toLocaleString()}
|
||||
@ -202,7 +235,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Jobs in 10 yrs</td>
|
||||
<td className="px-3 py-2 border-b font-semibold">
|
||||
Jobs in 10 yrs
|
||||
</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.projection.toLocaleString()}
|
||||
@ -215,7 +250,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Growth %</td>
|
||||
<td className="px-3 py-2 border-b font-semibold">Growth %</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.percentChange}%
|
||||
@ -228,7 +263,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Annual Openings</td>
|
||||
<td className="px-3 py-2 border-b font-semibold">
|
||||
Annual Openings
|
||||
</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
|
||||
@ -244,18 +281,19 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
</table>
|
||||
|
||||
{/* Conditional disclaimer when AI risk is Moderate or High */}
|
||||
{(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
|
||||
{aiRisk?.riskLevel &&
|
||||
(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
|
||||
<p className="text-sm text-red-600 mt-2">
|
||||
Note: These 10-year projections may change if AI-driven
|
||||
tools significantly affect {careerDetails.title} tasks.
|
||||
With a <strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk,
|
||||
it’s possible that some tasks or responsibilities could be automated
|
||||
over time.
|
||||
Note: These 10‑year projections may change if AI‑driven tools
|
||||
significantly affect {careerDetails.title} tasks. With a
|
||||
<strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk, it’s possible
|
||||
some responsibilities could be automated over time.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user