Limited Data flag issues fixed-ish. Loading bar to stretch most of the width of container.
This commit is contained in:
parent
a1a8f9c7dc
commit
ca7b230b25
@ -30,8 +30,6 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle
|
||||
|
||||
const careerPromises = careerSuggestions.map(async (career) => {
|
||||
try {
|
||||
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
@ -49,45 +47,57 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle
|
||||
}
|
||||
};
|
||||
|
||||
// Step 1: Fetch CIP Code
|
||||
const cipData = await fetchJSON(`${apiUrl}/cip/${career.code}`);
|
||||
const isCipMissing = !cipData || Object.keys(cipData).length === 0;
|
||||
// Fetch Data in Parallel
|
||||
const [cipData, jobDetailsData, economicData, salaryResponse] = await Promise.all([
|
||||
fetchJSON(`${apiUrl}/cip/${career.code}`),
|
||||
fetchJSON(`${apiUrl}/onet/career-description/${career.code}`),
|
||||
fetchJSON(`${apiUrl}/projections/${career.code.split('.')[0]}`),
|
||||
axios.get(`${apiUrl}/salary`, {
|
||||
params: { socCode: career.code.split('.')[0], area: areaTitle },
|
||||
headers,
|
||||
}).then((res) => {
|
||||
updateProgress();
|
||||
return res.data;
|
||||
}).catch((error) => {
|
||||
updateProgress();
|
||||
if (error.response?.status === 404) {
|
||||
console.warn(`⚠️ Salary data missing for ${career.title} (${career.code})`);
|
||||
return null;
|
||||
}
|
||||
return error.response;
|
||||
}),
|
||||
]);
|
||||
|
||||
// Step 2: Fetch Job Description & Tasks
|
||||
const jobDetailsData = await fetchJSON(`${apiUrl}/onet/career-description/${career.code}`);
|
||||
const isJobDetailsMissing = !jobDetailsData || Object.keys(jobDetailsData).length === 0;
|
||||
const isCipMissing = !cipData || Object.keys(cipData).length === 0;
|
||||
const isJobDetailsMissing = !jobDetailsData || Object.keys(jobDetailsData).length === 0;
|
||||
const isEconomicMissing = !economicData || Object.values(economicData).every(val => val === "N/A" || val === "*");
|
||||
const isSalaryMissing = salaryResponse === null || salaryResponse === undefined;
|
||||
|
||||
// Step 3: Fetch Salary & Economic Projections in Parallel
|
||||
const [economicData, salaryResponse] = await Promise.all([
|
||||
fetchJSON(`${apiUrl}/projections/${career.code.split('.')[0]}`),
|
||||
axios.get(`${apiUrl}/salary`, {
|
||||
params: { socCode: career.code.split('.')[0], area: areaTitle },
|
||||
headers,
|
||||
}).then((res) => {
|
||||
updateProgress();
|
||||
return res.data;
|
||||
}).catch((error) => {
|
||||
updateProgress();
|
||||
return error.response?.status === 404 ? null : error.response;
|
||||
}),
|
||||
]);
|
||||
|
||||
const isEconomicMissing = !economicData || Object.keys(economicData).length === 0;
|
||||
const isSalaryMissing = !salaryResponse;
|
||||
|
||||
const isLimitedData = isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing;
|
||||
|
||||
return { ...career, limitedData: isLimitedData };
|
||||
} catch (error) {
|
||||
console.error(`Error checking API response for ${career.title}:`, error);
|
||||
return { ...career, limitedData: true };
|
||||
// ✅ Log only when needed
|
||||
if (isSalaryMissing) {
|
||||
console.warn(`⚠️ Missing Salary Data for ${career.title} (${career.code})`);
|
||||
} else {
|
||||
console.log(`✅ Salary Data Available for ${career.title} (${career.code})`);
|
||||
}
|
||||
});
|
||||
|
||||
const isLimitedData = isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing;
|
||||
if (isLimitedData) console.log(`⚠️ Setting limitedData for ${career.title} (${career.code})`);
|
||||
|
||||
return { ...career, limitedData: isLimitedData };
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error checking API response for ${career.title}:`, error);
|
||||
return { ...career, limitedData: true };
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const updatedCareerList = await Promise.all(careerPromises);
|
||||
setUpdatedCareers(updatedCareerList);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
checkCareerDataAvailability();
|
||||
}, [careerSuggestions, apiUrl, userState, areaTitle]);
|
||||
@ -98,7 +108,9 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle
|
||||
|
||||
{loading ? (
|
||||
<div className="progress-container">
|
||||
<div className="progress-bar" style={{ width: `${progress}%` }}>
|
||||
<div className="progress-bar" style={{
|
||||
width: `${progress}%`,
|
||||
maxWidth: "100%", }}>
|
||||
{Math.round(progress)}%
|
||||
</div>
|
||||
<p>Loading Career Suggestions...</p>
|
||||
|
@ -22,11 +22,11 @@ const Chatbot = ({ context }) => {
|
||||
Your role is to not only provide career suggestions but to analyze them based on salary potential, job stability, education costs, and market trends.
|
||||
|
||||
Use the following user-specific data:
|
||||
- Career Suggestions: ${context.careerSuggestions.map((c) => c.title).join(", ") || "No suggestions available."}
|
||||
- Selected Career: ${context.selectedCareer?.title || "None"}
|
||||
- Schools: ${context.schools.map((s) => s["INSTNM"]).join(", ") || "No schools available."}
|
||||
- Career Suggestions: ${context.data.careerSuggestions?.map((c) => c.title).join(", ") || "No suggestions available."}
|
||||
- Selected Career: ${context.data.selectedCareer?.title || "None"}
|
||||
- Schools: ${context.data.schools?.map((s) => s["INSTNM"]).join(", ") || "No schools available."}
|
||||
- Median Salary: ${
|
||||
context.salaryData.find((s) => s.percentile === "Median")?.value || "Unavailable"
|
||||
context.data.salaryData?.find((s) => s.percentile === "Median")?.value || "Unavailable"
|
||||
}
|
||||
- ROI (Return on Investment): If available, use education costs vs. salary potential to guide users.
|
||||
|
||||
@ -41,7 +41,6 @@ const Chatbot = ({ context }) => {
|
||||
- "If you prefer stability, Y is a better long-term bet."
|
||||
`;
|
||||
|
||||
|
||||
const messagesToSend = [
|
||||
{ role: "system", content: contextSummary }, // Inject AptivaAI data on every request
|
||||
...messages,
|
||||
|
@ -54,7 +54,7 @@ h2 {
|
||||
/* Career Suggestions Section */
|
||||
.career-suggestions-container {
|
||||
flex: 1.5; /* Ensures it takes the majority of space */
|
||||
width: 60%;
|
||||
width: 100%;
|
||||
max-width: 75%;
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
@ -115,6 +115,8 @@ h2 {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
position: relative; /* Ensures proper layout */
|
||||
overflow: hidden; /* Prevents extra spacing */
|
||||
}
|
||||
|
||||
/* Striped Progress Bar */
|
||||
|
@ -35,6 +35,7 @@ function Dashboard() {
|
||||
const [selectedJobZone, setSelectedJobZone] = useState('');
|
||||
const [careersWithJobZone, setCareersWithJobZone] = useState([]); // Store careers with job zone info
|
||||
const [selectedFit, setSelectedFit] = useState('');
|
||||
const [results, setResults] = useState([]); // Add results state
|
||||
|
||||
const jobZoneLabels = {
|
||||
'1': 'Little or No Preparation',
|
||||
@ -107,6 +108,7 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
||||
loading={loading}
|
||||
error={error}
|
||||
userState={userState}
|
||||
results={results}
|
||||
/>
|
||||
);
|
||||
}, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]);
|
||||
@ -153,84 +155,114 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
||||
fetchUserProfile();
|
||||
}, [apiUrl]);
|
||||
|
||||
const handleCareerClick = useCallback(
|
||||
async (career) => {
|
||||
const socCode = career.code; // Extract SOC code from career object
|
||||
setSelectedCareer(career); // Set career first to trigger loading panel
|
||||
setLoading(true); // Enable loading state only when career is clicked
|
||||
setError(null); // Clear previous errors
|
||||
setCareerDetails({}); // Reset career details to avoid undefined errors
|
||||
setSchools([]); // Reset schools
|
||||
setSalaryData([]); // Reset salary data
|
||||
setEconomicProjections({}); // Reset economic projections
|
||||
setTuitionData([]); // Reset tuition data
|
||||
const handleCareerClick = useCallback(
|
||||
async (career) => {
|
||||
const socCode = career.code; // Extract SOC code from career object
|
||||
setSelectedCareer(career); // Set career first to trigger loading panel
|
||||
setLoading(true); // Enable loading state only when career is clicked
|
||||
setError(null); // Clear previous errors
|
||||
setCareerDetails({}); // Reset career details to avoid undefined errors
|
||||
setSchools([]); // Reset schools
|
||||
setSalaryData([]); // Reset salary data
|
||||
setEconomicProjections({}); // Reset economic projections
|
||||
setTuitionData([]); // Reset tuition data
|
||||
|
||||
|
||||
if (!socCode) {
|
||||
console.error('SOC Code is missing');
|
||||
setError('SOC Code is missing');
|
||||
return;
|
||||
}
|
||||
if (!socCode) {
|
||||
console.error('SOC Code is missing');
|
||||
setError('SOC Code is missing');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Fetch CIP Code
|
||||
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);
|
||||
|
||||
// Step 2: Fetch Job Description and Tasks
|
||||
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();
|
||||
|
||||
// Step 3: Fetch Data in Parallel for other career details
|
||||
// Salary API call with error handling
|
||||
let salaryResponse;
|
||||
try {
|
||||
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle }});
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Salary data not available for ${career.title} (${socCode})`);
|
||||
salaryResponse = { data: {} }; // Prevents breaking the whole update
|
||||
}
|
||||
|
||||
// Projections API call with error handling
|
||||
let economicResponse;
|
||||
try {
|
||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Economic projections not available for ${career.title} (${socCode})`);
|
||||
economicResponse = { data: {} }; // Prevents breaking the whole update
|
||||
}
|
||||
|
||||
// Tuition API call with error handling
|
||||
let tuitionResponse;
|
||||
try {
|
||||
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState }});
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Tuition data not available for ${career.title} (${socCode})`);
|
||||
tuitionResponse = { data: {} };
|
||||
}
|
||||
|
||||
// Fetch schools separately (this one seems to be working fine)
|
||||
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
|
||||
|
||||
// Handle Distance Calculation
|
||||
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||
try {
|
||||
// Step 1: Fetch CIP Code
|
||||
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);
|
||||
|
||||
// Step 2: Fetch Job Description and Tasks
|
||||
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();
|
||||
|
||||
// Step 3: Fetch Data in Parallel for other career details
|
||||
const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
|
||||
fetchSchools(cleanedCipCode, userState),
|
||||
axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`),
|
||||
axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState }}),
|
||||
axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle }}),
|
||||
]);
|
||||
|
||||
// Handle Distance Calculation
|
||||
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||
const response = await axios.post(`${apiUrl}/maps/distance`, {
|
||||
userZipcode,
|
||||
destinations: schoolAddress,
|
||||
});
|
||||
const { distance, duration } = response.data;
|
||||
return { ...school, distance, duration };
|
||||
}));
|
||||
|
||||
// Process Salary Data
|
||||
const salaryDataPoints = [
|
||||
{ percentile: '10th Percentile', value: salaryResponse.data.A_PCT10 || 0 },
|
||||
{ percentile: '25th Percentile', value: salaryResponse.data.A_PCT25 || 0 },
|
||||
{ percentile: 'Median', value: salaryResponse.data.A_MEDIAN || 0 },
|
||||
{ percentile: '75th Percentile', value: salaryResponse.data.A_PCT75 || 0 },
|
||||
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
|
||||
];
|
||||
|
||||
// Consolidate Career Details with Job Description and Tasks
|
||||
setCareerDetails({
|
||||
...career,
|
||||
jobDescription: description,
|
||||
tasks: tasks,
|
||||
economicProjections: economicResponse.data,
|
||||
salaryData: salaryDataPoints,
|
||||
schools: schoolsWithDistance,
|
||||
tuitionData: tuitionResponse.data,
|
||||
const response = await axios.post(`${apiUrl}/maps/distance`, {
|
||||
userZipcode,
|
||||
destinations: schoolAddress,
|
||||
});
|
||||
const { distance, duration } = response.data;
|
||||
return { ...school, distance, duration };
|
||||
} catch (error) {
|
||||
console.error('Error processing career click:', error.message);
|
||||
setError('Failed to load data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
console.warn(`⚠️ Distance calculation failed for ${school.INSTNM}`);
|
||||
return { ...school, distance: 'N/A', duration: 'N/A' };
|
||||
}
|
||||
},
|
||||
[userState, apiUrl, areaTitle, userZipcode]
|
||||
);
|
||||
}));
|
||||
|
||||
// Process Salary Data
|
||||
const salaryDataPoints = salaryResponse.data && Object.keys(salaryResponse.data).length > 0 ? [
|
||||
{ percentile: '10th Percentile', value: salaryResponse.data.A_PCT10 || 0 },
|
||||
{ percentile: '25th Percentile', value: salaryResponse.data.A_PCT25 || 0 },
|
||||
{ percentile: 'Median', value: salaryResponse.data.A_MEDIAN || 0 },
|
||||
{ percentile: '75th Percentile', value: salaryResponse.data.A_PCT75 || 0 },
|
||||
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
|
||||
] : [];
|
||||
|
||||
setCareerDetails({
|
||||
...career,
|
||||
jobDescription: description,
|
||||
tasks: tasks,
|
||||
economicProjections: economicResponse.data || {},
|
||||
salaryData: salaryDataPoints,
|
||||
schools: schoolsWithDistance,
|
||||
tuitionData: tuitionResponse.data || [],
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing career click:', error.message);
|
||||
setError('Failed to load data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[userState, apiUrl, areaTitle, userZipcode]
|
||||
);
|
||||
|
||||
console.log('Updated careerDetails:', careerDetails);
|
||||
|
||||
const chartData = {
|
||||
labels: riaSecScores.map((score) => score.area),
|
||||
@ -322,6 +354,7 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
|
||||
userState,
|
||||
areaTitle,
|
||||
userZipcode,
|
||||
results, // Pass results to Chatbot
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -11,43 +11,28 @@ function PopoutPanel({
|
||||
error = null,
|
||||
closePanel,
|
||||
}) {
|
||||
|
||||
const [isCalculated, setIsCalculated] = useState(false);
|
||||
const [results, setResults] = useState([]); // Store loan repayment calculation results
|
||||
const [loadingCalculation, setLoadingCalculation] = useState(false);
|
||||
const [persistedROI, setPersistedROI] = useState({});
|
||||
|
||||
const {
|
||||
jobDescription = null,
|
||||
tasks = null,
|
||||
title = 'Career Details',
|
||||
economicProjections = {},
|
||||
salaryData = [],
|
||||
schools = [],
|
||||
} = data || {};
|
||||
const {
|
||||
jobDescription = null,
|
||||
tasks = null,
|
||||
title = 'Career Details',
|
||||
economicProjections = {},
|
||||
salaryData = [],
|
||||
schools = [],
|
||||
} = data || {};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
useEffect(() => {
|
||||
setResults([]);
|
||||
setIsCalculated(false);
|
||||
}, [schools]);
|
||||
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
if (loading || loadingCalculation) {
|
||||
return (
|
||||
<div className="popout-panel">
|
||||
<button className="close-btn" onClick={closePanel}>X</button>
|
||||
<h2>Loading Career Details...</h2>
|
||||
<ClipLoader size={35} color="#4A90E2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
setResults([]);
|
||||
setIsCalculated(false);
|
||||
}, [schools]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
// Handle loading state
|
||||
if (loading || loadingCalculation) {
|
||||
return (
|
||||
<div className="popout-panel">
|
||||
@ -76,7 +61,7 @@ if (loading || loadingCalculation) {
|
||||
|
||||
return (
|
||||
<div className="popout-panel">
|
||||
<button onClick={closePanel}>Close</button>
|
||||
<button onClick={handleClosePanel}>Close</button>
|
||||
<h2>{title}</h2>
|
||||
|
||||
{/* Job Description and Tasks */}
|
||||
@ -98,8 +83,8 @@ if (loading || loadingCalculation) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Economic Projections */}
|
||||
<div className="economic-projections">
|
||||
{/* Economic Projections */}
|
||||
<div className="economic-projections">
|
||||
<h3>Economic Projections for {userState}</h3>
|
||||
{economicProjections && typeof economicProjections === 'object' ? (
|
||||
<ul>
|
||||
@ -140,7 +125,7 @@ if (loading || loadingCalculation) {
|
||||
{/* Schools Offering Programs Section */}
|
||||
<h3>Schools Offering Programs</h3>
|
||||
<div className="schools-offering">
|
||||
{schools.length > 0 ? (
|
||||
{schools.length > 0 ? (
|
||||
schools.map((school, index) => (
|
||||
<div key={index} className="school-card">
|
||||
<div><strong>{school['INSTNM']}</strong></div>
|
||||
|
Loading…
Reference in New Issue
Block a user