Limited Data flag issues fixed-ish. Loading bar to stretch most of the width of container.

This commit is contained in:
Josh 2025-03-10 17:40:12 +00:00
parent 96917b86b3
commit 785cac861b
5 changed files with 176 additions and 145 deletions

View File

@ -13,7 +13,7 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle
if (!careerSuggestions || careerSuggestions.length === 0) {
setLoading(false);
return;
}
}
const token = localStorage.getItem('token'); // Get auth token
@ -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>

View File

@ -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,

View File

@ -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 */

View File

@ -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>

View File

@ -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">
@ -57,7 +42,7 @@ if (loading || loadingCalculation) {
</div>
);
}
// Get program length for calculating tuition
const getProgramLength = (degreeType) => {
if (degreeType?.includes("Associate")) return 2;
@ -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>