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
|
SERVER1_PORT=5000
|
||||||
SERVER2_PORT=5001
|
SERVER2_PORT=5001
|
||||||
SERVER3_PORT=5002
|
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 CareerSearch from './CareerSearch.js';
|
||||||
import { Button } from './ui/button.js';
|
import { Button } from './ui/button.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import isAllOther from '../utils/isAllOther.js';
|
||||||
|
|
||||||
const STATES = [
|
const STATES = [
|
||||||
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
|
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
|
||||||
@ -338,181 +339,103 @@ function CareerExplorer() {
|
|||||||
}
|
}
|
||||||
}, [location.state, userProfile, fetchSuggestions, navigate]);
|
}, [location.state, userProfile, fetchSuggestions, navigate]);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
/* ------------------------------------------------------
|
||||||
// handleCareerClick (detail fetch for CIP, Salary, etc.)
|
handleCareerClick – fetches all details for one career
|
||||||
// ------------------------------------------------------
|
---------------------------------------------------- */
|
||||||
const handleCareerClick = useCallback(
|
const handleCareerClick = useCallback(
|
||||||
async (career) => {
|
async (career) => {
|
||||||
console.log('[handleCareerClick] career object:', JSON.stringify(career, null, 2));
|
|
||||||
|
|
||||||
const socCode = career.code;
|
const socCode = career.code;
|
||||||
|
if (!socCode) return;
|
||||||
|
|
||||||
setSelectedCareer(career);
|
setSelectedCareer(career);
|
||||||
setError(null);
|
setCareerDetails(null); // reset any previous modal
|
||||||
setCareerDetails(null);
|
|
||||||
setSalaryData([]);
|
|
||||||
setEconomicProjections({});
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
if (!socCode) {
|
|
||||||
console.error('SOC Code is missing');
|
|
||||||
setError('SOC Code is missing');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) CIP fetch
|
/* ---------- 1. CIP lookup ---------- */
|
||||||
const cipResponse = await fetch(`/api/cip/${socCode}`);
|
let cipCode = null;
|
||||||
if (!cipResponse.ok) {
|
const cipRes = await fetch(`/api/cip/${socCode}`);
|
||||||
setError(
|
if (cipRes.ok) {
|
||||||
`We're sorry, but specific details for "${career.title}" are not available at this time.`
|
cipCode = (await cipRes.json()).cipCode ?? null;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
/* ---------- 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 ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 3. Salary data ---------- */
|
||||||
|
const salaryRes = await axios.get('/api/salary', {
|
||||||
|
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
||||||
|
}).catch(() => ({ data: {} }));
|
||||||
|
|
||||||
|
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);
|
||||||
|
const projRes = await axios.get(
|
||||||
|
`/api/projections/${socCode.split('.')[0]}`,
|
||||||
|
{ 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({
|
setCareerDetails({
|
||||||
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
return; // stops here – nothing useful to show
|
||||||
return;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const { description, tasks } = await jobDetailsResponse.json();
|
|
||||||
|
|
||||||
// 3) Salary data
|
|
||||||
let salaryResponse;
|
|
||||||
try {
|
|
||||||
salaryResponse = await axios.get('/api/salary', {
|
|
||||||
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
salaryResponse = { data: {} };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sData = salaryResponse.data || {};
|
/* ---------- 6. AI‑risk (only if job details exist) ---------- */
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// 4) Economic Projections
|
|
||||||
const fullStateName = getFullStateName(userState); // your helper
|
|
||||||
let economicResponse = { data: {} };
|
|
||||||
try {
|
|
||||||
economicResponse = await axios.get(
|
|
||||||
`/api/projections/${socCode.split('.')[0]}`,
|
|
||||||
{
|
|
||||||
params: { state: fullStateName },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
economicResponse = { data: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------
|
|
||||||
// 5) AI RISK ANALYSIS LOGIC
|
|
||||||
// Attempt to retrieve from server2 first;
|
|
||||||
// if not found => call server3 => store in server2.
|
|
||||||
// ----------------------------------------------------
|
|
||||||
let aiRisk = null;
|
let aiRisk = null;
|
||||||
const strippedSocCode = socCode.split('.')[0];
|
if (haveJobInfo) {
|
||||||
|
try {
|
||||||
|
aiRisk = (await axios.get(`/api/ai-risk/${socCode}`)).data;
|
||||||
try {
|
} catch (err) {
|
||||||
// Check local DB first (SQLite -> server2)
|
if (err.response?.status === 404) {
|
||||||
const localRiskRes = await axios.get(`/api/ai-risk/${socCode}`);
|
|
||||||
aiRisk = localRiskRes.data;
|
|
||||||
} catch (err) {
|
|
||||||
// If 404, we call server3's ChatGPT route at the SAME base url
|
|
||||||
if (err.response && err.response.status === 404) {
|
|
||||||
try {
|
|
||||||
const aiRes = await axios.post('/api/public/ai-risk-analysis', {
|
const aiRes = await axios.post('/api/public/ai-risk-analysis', {
|
||||||
socCode,
|
socCode,
|
||||||
careerName: career.title,
|
careerName : career.title,
|
||||||
jobDescription: description,
|
jobDescription : description,
|
||||||
tasks,
|
tasks,
|
||||||
});
|
});
|
||||||
|
aiRisk = aiRes.data;
|
||||||
const { riskLevel, reasoning } = aiRes.data;
|
// store for next time (best‑effort)
|
||||||
|
axios.post('/api/ai-risk', aiRisk).catch(() => {});
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error('Error fetching AI risk from server2:', err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Build final details object
|
/* ---------- 7. Build the final object & show modal ---------- */
|
||||||
const updatedCareerDetails = {
|
const updatedCareerDetails = {
|
||||||
...career,
|
...career,
|
||||||
|
cipCode, // may be null – EducationalPrograms handles it
|
||||||
jobDescription: description,
|
jobDescription: description,
|
||||||
tasks,
|
tasks,
|
||||||
salaryData: salaryDataPoints,
|
salaryData: salaryDataPoints,
|
||||||
economicProjections: economicResponse.data || {},
|
economicProjections: projRes.data ?? {},
|
||||||
aiRisk, // <--- Now we have it attached
|
aiRisk,
|
||||||
};
|
};
|
||||||
|
|
||||||
setCareerDetails(updatedCareerDetails);
|
setCareerDetails(updatedCareerDetails);
|
||||||
|
} catch (e) {
|
||||||
} catch (error) {
|
console.error('[handleCareerClick] fatal:', e);
|
||||||
console.error('Error processing career click:', error.message);
|
|
||||||
setCareerDetails({
|
setCareerDetails({
|
||||||
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
||||||
});
|
});
|
||||||
@ -520,9 +443,10 @@ function CareerExplorer() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[userState, areaTitle]
|
[areaTitle, userState]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// handleCareerFromSearch
|
// handleCareerFromSearch
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@ -1071,7 +995,7 @@ const handleSelectForEducation = (career) => {
|
|||||||
{/* Legend container with less internal gap, plus a left margin */}
|
{/* Legend container with less internal gap, plus a left margin */}
|
||||||
<div className="flex items-center gap-1 ml-4">
|
<div className="flex items-center gap-1 ml-4">
|
||||||
<span className="warning-icon">⚠️</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -11,23 +11,36 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
const aiRisk = careerDetails?.aiRisk || null;
|
const aiRisk = careerDetails?.aiRisk || null;
|
||||||
|
|
||||||
|
|
||||||
console.log('CareerModal props:', { career, careerDetails, aiRisk });
|
if (!careerDetails) {
|
||||||
|
|
||||||
// Handle your normal careerDetails loading logic
|
|
||||||
if (careerDetails?.error) {
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
<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">
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
<p className="text-lg text-gray-700 mb-4">{careerDetails.error}</p>
|
<p className="text-lg text-gray-700">Loading career details…</p>
|
||||||
<button onClick={closeModal} className="bg-red-500 text-white p-2 rounded">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</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 (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
<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">
|
<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
|
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
|
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
|
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
|
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
|
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!
|
add it to your comparison list or move straight into Preparing & Upskilling for Your Career!
|
||||||
@ -80,20 +93,22 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
{careerDetails.title}
|
{careerDetails.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* AI RISK SECTION */}
|
{/* AI RISK SECTION */}
|
||||||
{loadingRisk ? (
|
{loadingRisk && (
|
||||||
<p className="text-sm text-gray-500 mt-1">Loading AI risk...</p>
|
<p className="text-sm text-gray-500 mt-1">Loading AI risk…</p>
|
||||||
) : aiRisk?.riskLevel && aiRisk?.reasoning ? (
|
)}
|
||||||
<div className="text-sm text-gray-500 mt-1">
|
|
||||||
<strong>AI Risk Level:</strong> {aiRisk.riskLevel}
|
{!loadingRisk && aiRisk && aiRisk.riskLevel && aiRisk.reasoning && (
|
||||||
<br />
|
<div className="text-sm text-gray-500 mt-1">
|
||||||
<span>{aiRisk.reasoning}</span>
|
<strong>AI Risk Level:</strong> {aiRisk.riskLevel}
|
||||||
</div>
|
<br />
|
||||||
) : (
|
<span>{aiRisk.reasoning}</span>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
</div>
|
||||||
No AI risk data available
|
)}
|
||||||
</p>
|
|
||||||
)}
|
{!loadingRisk && !aiRisk && (
|
||||||
|
<p className="text-sm text-gray-500 mt-1">No AI risk data available</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
@ -126,136 +141,159 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Job Description */}
|
{/* Job Description */}
|
||||||
<div className="mb-4">
|
{careerDetails.jobDescription && (
|
||||||
<h3 className="text-lg font-semibold mb-1">Job Description:</h3>
|
<div className="mb-4">
|
||||||
<p className="text-gray-700">{careerDetails.jobDescription}</p>
|
<h3 className="text-lg font-semibold mb-1">Job Description:</h3>
|
||||||
</div>
|
<p className="text-gray-700">{careerDetails.jobDescription}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tasks */}
|
{/* Tasks */}
|
||||||
<div className="mb-4 border-t pt-3">
|
{careerDetails.tasks?.length > 0 && (
|
||||||
<h3 className="text-lg font-semibold mb-2">Tasks:</h3>
|
<div className="mb-4 border-t pt-3">
|
||||||
<ul className="list-disc pl-5 space-y-1">
|
<h3 className="text-lg font-semibold mb-2">Tasks:</h3>
|
||||||
{careerDetails.tasks.map((task, i) => (
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
<li key={i}>{task}</li>
|
{careerDetails.tasks.map((task, i) => (
|
||||||
))}
|
<li key={i}>{task}</li>
|
||||||
</ul>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
{/* Salary & 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>
|
</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"></th>
|
|
||||||
{careerDetails.economicProjections.state && (
|
|
||||||
<th className="px-3 py-2 border-b">
|
|
||||||
{careerDetails.economicProjections.state.area}
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
{careerDetails.economicProjections.national && (
|
|
||||||
<th className="px-3 py-2 border-b">National</th>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<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()}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{careerDetails.economicProjections.national && (
|
|
||||||
<td className="px-3 py-2 border-b">
|
|
||||||
{careerDetails.economicProjections.national.base.toLocaleString()}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<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()}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{careerDetails.economicProjections.national && (
|
|
||||||
<td className="px-3 py-2 border-b">
|
|
||||||
{careerDetails.economicProjections.national.projection.toLocaleString()}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<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}%
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{careerDetails.economicProjections.national && (
|
|
||||||
<td className="px-3 py-2 border-b">
|
|
||||||
{careerDetails.economicProjections.national.percentChange}%
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<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()}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{careerDetails.economicProjections.national && (
|
|
||||||
<td className="px-3 py-2 border-b">
|
|
||||||
{careerDetails.economicProjections.national.annualOpenings.toLocaleString()}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{/* Conditional disclaimer when AI risk is Moderate or High */}
|
{(careerDetails.salaryData?.length > 0 ||
|
||||||
{(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
|
(careerDetails.economicProjections &&
|
||||||
<p className="text-sm text-red-600 mt-2">
|
(careerDetails.economicProjections.state ||
|
||||||
Note: These 10-year projections may change if AI-driven
|
careerDetails.economicProjections.national))) && (
|
||||||
tools significantly affect {careerDetails.title} tasks.
|
|
||||||
With a <strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk,
|
<div className="flex flex-col md:flex-row gap-4 border-t pt-3">
|
||||||
it’s possible that some tasks or responsibilities could be automated
|
|
||||||
over time.
|
|
||||||
</p>
|
|
||||||
|
{/* ── 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>
|
||||||
|
<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((row, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="px-3 py-2 border-b">{row.percentile}</td>
|
||||||
|
<td className="px-3 py-2 border-b">
|
||||||
|
${row.regionalSalary.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 border-b">
|
||||||
|
${row.nationalSalary.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── 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">
|
||||||
|
<thead className="bg-gray-100">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 border-b"></th>
|
||||||
|
{careerDetails.economicProjections.state && (
|
||||||
|
<th className="px-3 py-2 border-b">
|
||||||
|
{careerDetails.economicProjections.state.area}
|
||||||
|
</th>
|
||||||
)}
|
)}
|
||||||
</div>
|
{careerDetails.economicProjections.national && (
|
||||||
|
<th className="px-3 py-2 border-b">National</th>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<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()}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{careerDetails.economicProjections.national && (
|
||||||
|
<td className="px-3 py-2 border-b">
|
||||||
|
{careerDetails.economicProjections.national.base.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<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()}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{careerDetails.economicProjections.national && (
|
||||||
|
<td className="px-3 py-2 border-b">
|
||||||
|
{careerDetails.economicProjections.national.projection.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<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}%
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{careerDetails.economicProjections.national && (
|
||||||
|
<td className="px-3 py-2 border-b">
|
||||||
|
{careerDetails.economicProjections.national.percentChange}%
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<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()}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{careerDetails.economicProjections.national && (
|
||||||
|
<td className="px-3 py-2 border-b">
|
||||||
|
{careerDetails.economicProjections.national.annualOpenings.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{/* Conditional disclaimer when AI risk is Moderate or 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
|
||||||
|
some responsibilities could be automated over time.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user