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 a1a8f9c7dc
commit ca7b230b25
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) { if (!careerSuggestions || careerSuggestions.length === 0) {
setLoading(false); setLoading(false);
return; return;
} }
const token = localStorage.getItem('token'); // Get auth token const token = localStorage.getItem('token'); // Get auth token
@ -30,8 +30,6 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle
const careerPromises = careerSuggestions.map(async (career) => { const careerPromises = careerSuggestions.map(async (career) => {
try { try {
const headers = { const headers = {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
Accept: 'application/json', Accept: 'application/json',
@ -49,45 +47,57 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle
} }
}; };
// Step 1: Fetch CIP Code // Fetch Data in Parallel
const cipData = await fetchJSON(`${apiUrl}/cip/${career.code}`); const [cipData, jobDetailsData, economicData, salaryResponse] = await Promise.all([
const isCipMissing = !cipData || Object.keys(cipData).length === 0; 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 isCipMissing = !cipData || Object.keys(cipData).length === 0;
const jobDetailsData = await fetchJSON(`${apiUrl}/onet/career-description/${career.code}`); const isJobDetailsMissing = !jobDetailsData || Object.keys(jobDetailsData).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 // ✅ Log only when needed
const [economicData, salaryResponse] = await Promise.all([ if (isSalaryMissing) {
fetchJSON(`${apiUrl}/projections/${career.code.split('.')[0]}`), console.warn(`⚠️ Missing Salary Data for ${career.title} (${career.code})`);
axios.get(`${apiUrl}/salary`, { } else {
params: { socCode: career.code.split('.')[0], area: areaTitle }, console.log(`✅ Salary Data Available for ${career.title} (${career.code})`);
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 };
} }
});
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); const updatedCareerList = await Promise.all(careerPromises);
setUpdatedCareers(updatedCareerList); setUpdatedCareers(updatedCareerList);
} finally {
setLoading(false); setLoading(false);
}; }
};
checkCareerDataAvailability(); checkCareerDataAvailability();
}, [careerSuggestions, apiUrl, userState, areaTitle]); }, [careerSuggestions, apiUrl, userState, areaTitle]);
@ -98,7 +108,9 @@ export function CareerSuggestions({ careerSuggestions = [], userState, areaTitle
{loading ? ( {loading ? (
<div className="progress-container"> <div className="progress-container">
<div className="progress-bar" style={{ width: `${progress}%` }}> <div className="progress-bar" style={{
width: `${progress}%`,
maxWidth: "100%", }}>
{Math.round(progress)}% {Math.round(progress)}%
</div> </div>
<p>Loading Career Suggestions...</p> <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. 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: Use the following user-specific data:
- Career Suggestions: ${context.careerSuggestions.map((c) => c.title).join(", ") || "No suggestions available."} - Career Suggestions: ${context.data.careerSuggestions?.map((c) => c.title).join(", ") || "No suggestions available."}
- Selected Career: ${context.selectedCareer?.title || "None"} - Selected Career: ${context.data.selectedCareer?.title || "None"}
- Schools: ${context.schools.map((s) => s["INSTNM"]).join(", ") || "No schools available."} - Schools: ${context.data.schools?.map((s) => s["INSTNM"]).join(", ") || "No schools available."}
- Median Salary: ${ - 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. - 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." - "If you prefer stability, Y is a better long-term bet."
`; `;
const messagesToSend = [ const messagesToSend = [
{ role: "system", content: contextSummary }, // Inject AptivaAI data on every request { role: "system", content: contextSummary }, // Inject AptivaAI data on every request
...messages, ...messages,

View File

@ -54,7 +54,7 @@ h2 {
/* Career Suggestions Section */ /* Career Suggestions Section */
.career-suggestions-container { .career-suggestions-container {
flex: 1.5; /* Ensures it takes the majority of space */ flex: 1.5; /* Ensures it takes the majority of space */
width: 60%; width: 100%;
max-width: 75%; max-width: 75%;
background-color: #ffffff; background-color: #ffffff;
padding: 15px; padding: 15px;
@ -115,6 +115,8 @@ h2 {
margin: 20px 0; margin: 20px 0;
padding: 10px; padding: 10px;
text-align: center; text-align: center;
position: relative; /* Ensures proper layout */
overflow: hidden; /* Prevents extra spacing */
} }
/* Striped Progress Bar */ /* Striped Progress Bar */

View File

@ -35,6 +35,7 @@ function Dashboard() {
const [selectedJobZone, setSelectedJobZone] = useState(''); const [selectedJobZone, setSelectedJobZone] = useState('');
const [careersWithJobZone, setCareersWithJobZone] = useState([]); // Store careers with job zone info const [careersWithJobZone, setCareersWithJobZone] = useState([]); // Store careers with job zone info
const [selectedFit, setSelectedFit] = useState(''); const [selectedFit, setSelectedFit] = useState('');
const [results, setResults] = useState([]); // Add results state
const jobZoneLabels = { const jobZoneLabels = {
'1': 'Little or No Preparation', '1': 'Little or No Preparation',
@ -107,6 +108,7 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
loading={loading} loading={loading}
error={error} error={error}
userState={userState} userState={userState}
results={results}
/> />
); );
}, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]); }, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]);
@ -153,84 +155,114 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
fetchUserProfile(); fetchUserProfile();
}, [apiUrl]); }, [apiUrl]);
const handleCareerClick = useCallback( const handleCareerClick = useCallback(
async (career) => { async (career) => {
const socCode = career.code; // Extract SOC code from career object const socCode = career.code; // Extract SOC code from career object
setSelectedCareer(career); // Set career first to trigger loading panel setSelectedCareer(career); // Set career first to trigger loading panel
setLoading(true); // Enable loading state only when career is clicked setLoading(true); // Enable loading state only when career is clicked
setError(null); // Clear previous errors setError(null); // Clear previous errors
setCareerDetails({}); // Reset career details to avoid undefined errors setCareerDetails({}); // Reset career details to avoid undefined errors
setSchools([]); // Reset schools setSchools([]); // Reset schools
setSalaryData([]); // Reset salary data setSalaryData([]); // Reset salary data
setEconomicProjections({}); // Reset economic projections setEconomicProjections({}); // Reset economic projections
setTuitionData([]); // Reset tuition data setTuitionData([]); // Reset tuition data
if (!socCode) { if (!socCode) {
console.error('SOC Code is missing'); console.error('SOC Code is missing');
setError('SOC Code is missing'); setError('SOC Code is missing');
return; 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 { try {
// Step 1: Fetch CIP Code const response = await axios.post(`${apiUrl}/maps/distance`, {
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); userZipcode,
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code'); destinations: schoolAddress,
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 { distance, duration } = response.data;
return { ...school, distance, duration };
} catch (error) { } catch (error) {
console.error('Error processing career click:', error.message); console.warn(`⚠️ Distance calculation failed for ${school.INSTNM}`);
setError('Failed to load data'); return { ...school, distance: 'N/A', duration: 'N/A' };
} finally {
setLoading(false);
} }
}, }));
[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 = { const chartData = {
labels: riaSecScores.map((score) => score.area), labels: riaSecScores.map((score) => score.area),
@ -322,6 +354,7 @@ const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareer
userState, userState,
areaTitle, areaTitle,
userZipcode, userZipcode,
results, // Pass results to Chatbot
}} }}
/> />
</div> </div>

View File

@ -11,43 +11,28 @@ function PopoutPanel({
error = null, error = null,
closePanel, closePanel,
}) { }) {
const [isCalculated, setIsCalculated] = useState(false); const [isCalculated, setIsCalculated] = useState(false);
const [results, setResults] = useState([]); // Store loan repayment calculation results const [results, setResults] = useState([]); // Store loan repayment calculation results
const [loadingCalculation, setLoadingCalculation] = useState(false); const [loadingCalculation, setLoadingCalculation] = useState(false);
const [persistedROI, setPersistedROI] = useState({}); const [persistedROI, setPersistedROI] = useState({});
const { const {
jobDescription = null, jobDescription = null,
tasks = null, tasks = null,
title = 'Career Details', title = 'Career Details',
economicProjections = {}, economicProjections = {},
salaryData = [], salaryData = [],
schools = [], schools = [],
} = data || {}; } = data || {};
useEffect(() => {
useEffect(() => { setResults([]);
setResults([]); setIsCalculated(false);
setIsCalculated(false); }, [schools]);
}, [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>
);
}
if (!isVisible) return null; if (!isVisible) return null;
// Handle loading state
if (loading || loadingCalculation) { if (loading || loadingCalculation) {
return ( return (
<div className="popout-panel"> <div className="popout-panel">
@ -57,7 +42,7 @@ if (loading || loadingCalculation) {
</div> </div>
); );
} }
// Get program length for calculating tuition // Get program length for calculating tuition
const getProgramLength = (degreeType) => { const getProgramLength = (degreeType) => {
if (degreeType?.includes("Associate")) return 2; if (degreeType?.includes("Associate")) return 2;
@ -76,7 +61,7 @@ if (loading || loadingCalculation) {
return ( return (
<div className="popout-panel"> <div className="popout-panel">
<button onClick={closePanel}>Close</button> <button onClick={handleClosePanel}>Close</button>
<h2>{title}</h2> <h2>{title}</h2>
{/* Job Description and Tasks */} {/* Job Description and Tasks */}
@ -98,8 +83,8 @@ if (loading || loadingCalculation) {
)} )}
</div> </div>
{/* Economic Projections */} {/* Economic Projections */}
<div className="economic-projections"> <div className="economic-projections">
<h3>Economic Projections for {userState}</h3> <h3>Economic Projections for {userState}</h3>
{economicProjections && typeof economicProjections === 'object' ? ( {economicProjections && typeof economicProjections === 'object' ? (
<ul> <ul>
@ -140,7 +125,7 @@ if (loading || loadingCalculation) {
{/* Schools Offering Programs Section */} {/* Schools Offering Programs Section */}
<h3>Schools Offering Programs</h3> <h3>Schools Offering Programs</h3>
<div className="schools-offering"> <div className="schools-offering">
{schools.length > 0 ? ( {schools.length > 0 ? (
schools.map((school, index) => ( schools.map((school, index) => (
<div key={index} className="school-card"> <div key={index} className="school-card">
<div><strong>{school['INSTNM']}</strong></div> <div><strong>{school['INSTNM']}</strong></div>