From ca7b230b256560cfbb91e82fb77065b9a9c2672e Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 10 Mar 2025 17:40:12 +0000 Subject: [PATCH] Limited Data flag issues fixed-ish. Loading bar to stretch most of the width of container. --- src/components/CareerSuggestions.js | 84 +++++++------ src/components/Chatbot.js | 9 +- src/components/Dashboard.css | 4 +- src/components/Dashboard.js | 175 +++++++++++++++++----------- src/components/PopoutPanel.js | 49 +++----- 5 files changed, 176 insertions(+), 145 deletions(-) diff --git a/src/components/CareerSuggestions.js b/src/components/CareerSuggestions.js index 14368dd..b4b7dfe 100644 --- a/src/components/CareerSuggestions.js +++ b/src/components/CareerSuggestions.js @@ -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 ? (
-
+
{Math.round(progress)}%

Loading Career Suggestions...

diff --git a/src/components/Chatbot.js b/src/components/Chatbot.js index 967df59..6204440 100644 --- a/src/components/Chatbot.js +++ b/src/components/Chatbot.js @@ -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, diff --git a/src/components/Dashboard.css b/src/components/Dashboard.css index c3cb7ae..669a3ac 100644 --- a/src/components/Dashboard.css +++ b/src/components/Dashboard.css @@ -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 */ diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index 0d89937..9697f53 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -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 }} />
diff --git a/src/components/PopoutPanel.js b/src/components/PopoutPanel.js index 2bfe168..47dbc04 100644 --- a/src/components/PopoutPanel.js +++ b/src/components/PopoutPanel.js @@ -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 ( -
- -

Loading Career Details...

- -
- ); -} + setResults([]); + setIsCalculated(false); + }, [schools]); if (!isVisible) return null; - // Handle loading state if (loading || loadingCalculation) { return (
@@ -57,7 +42,7 @@ if (loading || loadingCalculation) {
); } - + // Get program length for calculating tuition const getProgramLength = (degreeType) => { if (degreeType?.includes("Associate")) return 2; @@ -76,7 +61,7 @@ if (loading || loadingCalculation) { return (
- +

{title}

{/* Job Description and Tasks */} @@ -98,8 +83,8 @@ if (loading || loadingCalculation) { )}
-{/* Economic Projections */} -
+ {/* Economic Projections */} +

Economic Projections for {userState}

{economicProjections && typeof economicProjections === 'object' ? (
    @@ -140,7 +125,7 @@ if (loading || loadingCalculation) { {/* Schools Offering Programs Section */}

    Schools Offering Programs

    - {schools.length > 0 ? ( + {schools.length > 0 ? ( schools.map((school, index) => (
    {school['INSTNM']}