dev1/src/components/Dashboard.js

489 lines
17 KiB
JavaScript

// Dashboard.js
import axios from 'axios';
import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
import { CareerSuggestions } from './CareerSuggestions.js';
import PopoutPanel from './PopoutPanel.js';
import MilestoneTracker from './MilestoneTracker.js'
import './Dashboard.css';
import Chatbot from "./Chatbot.js";
import { Bar } from 'react-chartjs-2';
import { fetchSchools } from '../utils/apiUtils.js';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
function Dashboard() {
const location = useLocation();
const navigate = useNavigate();
const [careerSuggestions, setCareerSuggestions] = useState([]);
const [careerDetails, setCareerDetails] = useState(null);
const [riaSecScores, setRiaSecScores] = useState([]);
const [selectedCareer, setSelectedCareer] = useState(null);
const [schools, setSchools] = useState([]);
const [salaryData, setSalaryData] = useState([]);
const [economicProjections, setEconomicProjections] = useState(null);
const [tuitionData, setTuitionData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [userState, setUserState] = useState(null);
const [areaTitle, setAreaTitle] = useState(null);
const [userZipcode, setUserZipcode] = useState(null);
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
const [selectedJobZone, setSelectedJobZone] = useState('');
const [careersWithJobZone, setCareersWithJobZone] = useState([]);
const [selectedFit, setSelectedFit] = useState('');
const [results, setResults] = useState([]);
const [chatbotContext, setChatbotContext] = useState({});
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
const [sessionHandled, setSessionHandled] = useState(false);
const handleUnauthorized = () => {
if (!sessionHandled) {
setSessionHandled(true);
setShowSessionExpiredModal(true); // Show session expired modal
}
};
// Function to handle the token check and fetch requests
const authFetch = async (url, options = {}, onUnauthorized) => {
const token = localStorage.getItem("token");
if (!token) {
console.log("Token is missing, triggering session expired modal.");
if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal
return null;
}
const finalOptions = {
...options,
headers: {
...(options.headers || {}),
Authorization: `Bearer ${token}`, // Attach the token to the request
},
};
try {
const res = await fetch(url, finalOptions);
// Log the response status for debugging
console.log("Response Status:", res.status);
if (res.status === 401 || res.status === 403) {
console.log("Session expired, triggering session expired modal.");
if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal
return null;
}
return res;
} catch (err) {
console.error("Fetch error:", err);
if (typeof onUnauthorized === 'function') onUnauthorized(); // Show session expired modal
return null;
}
};
// Fetch User Profile (with proper session handling)
const fetchUserProfile = async () => {
const res = await authFetch(`${apiUrl}/user-profile`);
if (!res) return;
if (res.ok) {
const profileData = await res.json();
setUserState(profileData.state);
setAreaTitle(profileData.area.trim() || '');
setUserZipcode(profileData.zipcode);
} else {
console.error('Failed to fetch user profile');
}
};
const jobZoneLabels = {
'1': 'Little or No Preparation',
'2': 'Some Preparation Needed',
'3': 'Medium Preparation Needed',
'4': 'Considerable Preparation Needed',
'5': 'Extensive Preparation Needed'
};
const fitLabels = {
'Best': 'Best - Very Strong Match',
'Great': 'Great - Strong Match',
'Good': 'Good - Less Strong Match'
};
const apiUrl = process.env.REACT_APP_API_URL || '';
useEffect(() => {
const fetchUserProfile = async () => {
const res = await authFetch(`${apiUrl}/user-profile`);
if (!res) return;
if (res.ok) {
const profileData = await res.json();
setUserState(profileData.state);
setAreaTitle(profileData.area.trim() || '');
setUserZipcode(profileData.zipcode);
} else {
console.error('Failed to fetch user profile');
}
};
fetchUserProfile();
}, [apiUrl]);
useEffect(() => {
const fetchJobZones = async () => {
if (careerSuggestions.length === 0) return;
const socCodes = careerSuggestions.map((career) => career.code);
try {
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
const jobZoneData = response.data;
const updatedCareers = careerSuggestions.map((career) => ({
...career,
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
}));
setCareersWithJobZone(updatedCareers);
} catch (error) {
console.error('Error fetching job zone information:', error);
}
};
fetchJobZones();
}, [careerSuggestions, apiUrl]);
const filteredCareers = useMemo(() => {
return careersWithJobZone.filter((career) => {
const jobZoneMatches = selectedJobZone
? career.job_zone !== null &&
career.job_zone !== undefined &&
typeof career.job_zone === 'number' &&
Number(career.job_zone) === Number(selectedJobZone)
: true;
const fitMatches = selectedFit ? career.fit === selectedFit : true;
return jobZoneMatches && fitMatches;
});
}, [careersWithJobZone, selectedJobZone, selectedFit]);
const updateChatbotContext = (updatedData) => {
setChatbotContext((prevContext) => {
const mergedContext = {
...prevContext,
...Object.keys(updatedData).reduce((acc, key) => {
if (updatedData[key] !== undefined && updatedData[key] !== null) {
acc[key] = updatedData[key];
}
return acc;
}, {}),
};
return mergedContext;
});
};
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
const memoizedPopoutPanel = useMemo(() => {
return (
<PopoutPanel
isVisible={!!selectedCareer}
data={careerDetails}
schools={schools}
salaryData={salaryData}
economicProjections={economicProjections}
tuitionData={tuitionData}
closePanel={() => setSelectedCareer(null)}
loading={loading}
error={error}
userState={userState}
results={results}
updateChatbotContext={updateChatbotContext}
/>
);
}, [selectedCareer, careerDetails, schools, salaryData, economicProjections, tuitionData, loading, error, userState]);
useEffect(() => {
let descriptions = [];
if (location.state) {
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
descriptions = scores.map((score) => score.description || "No description available.");
setCareerSuggestions(suggestions || []);
setRiaSecScores(scores || []);
setRiaSecDescriptions(descriptions);
} else {
console.warn('No data found, redirecting to Interest Inventory');
navigate('/interest-inventory');
}
}, [location.state, navigate]);
useEffect(() => {
if (
careerSuggestions.length > 0 &&
riaSecScores.length > 0 &&
userState !== null &&
areaTitle !== null &&
userZipcode !== null
) {
const newChatbotContext = {
careerSuggestions: [...careersWithJobZone],
riaSecScores: [...riaSecScores],
userState: userState || "",
areaTitle: areaTitle || "",
userZipcode: userZipcode || "",
};
setChatbotContext(newChatbotContext);
} else {
}
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
const handleCareerClick = useCallback(
async (career) => {
const socCode = career.code;
setSelectedCareer(career);
setLoading(true);
setError(null);
setCareerDetails({});
setSchools([]);
setSalaryData([]);
setEconomicProjections({});
setTuitionData([]);
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
return;
}
try {
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);
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();
let salaryResponse;
try {
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } });
} catch (error) {
salaryResponse = { data: {} };
}
let economicResponse;
try {
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
} catch (error) {
economicResponse = { data: {} };
}
let tuitionResponse;
try {
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } });
} catch (error) {
tuitionResponse = { data: {} };
}
const filteredSchools = await fetchSchools(cleanedCipCode, userState);
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
try {
const response = await axios.post(`${apiUrl}/maps/distance`, {
userZipcode,
destinations: schoolAddress,
});
const { distance, duration } = response.data;
return { ...school, distance, duration };
} catch (error) {
return { ...school, distance: 'N/A', duration: 'N/A' };
}
}));
const salaryDataPoints = salaryResponse.data && Object.keys(salaryResponse.data).length > 0
? [
{ percentile: "10th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT10, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT10, 10) || 0 },
{ percentile: "25th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT25, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT25, 10) || 0 },
{ percentile: "Median", regionalSalary: parseInt(salaryResponse.data.regional?.regional_MEDIAN, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_MEDIAN, 10) || 0 },
{ percentile: "75th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT75, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT75, 10) || 0 },
{ percentile: "90th Percentile", regionalSalary: parseInt(salaryResponse.data.regional?.regional_PCT90, 10) || 0, nationalSalary: parseInt(salaryResponse.data.national?.national_PCT90, 10) || 0 },
]
: [];
const updatedCareerDetails = {
...career,
jobDescription: description,
tasks: tasks,
economicProjections: economicResponse.data || {},
salaryData: salaryDataPoints,
schools: schoolsWithDistance,
tuitionData: tuitionResponse.data || [],
};
setCareerDetails(updatedCareerDetails);
updateChatbotContext({ careerDetails: updatedCareerDetails });
} catch (error) {
console.error('Error processing career click:', error.message);
setError('Failed to load data');
} finally {
setLoading(false);
}
},
[userState, apiUrl, areaTitle, userZipcode]
);
const chartData = {
labels: riaSecScores.map((score) => score.area),
datasets: [
{
label: 'RIASEC Scores',
data: riaSecScores.map((score) => score.score),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1,
},
],
};
return (
<div className="dashboard">
{showSessionExpiredModal && (
<div className="modal-overlay">
<div className="modal">
<h3>Session Expired</h3>
<p>Your session has expired or is invalid.</p>
<div className="modal-actions">
<button className="confirm-btn" onClick={() => setShowSessionExpiredModal(false)}>
Stay Signed In
</button>
<button className="confirm-btn" onClick={() => {
localStorage.removeItem("token");
localStorage.removeItem("UserId");
setShowSessionExpiredModal(false);
navigate("/signin");
}}>
Sign In Again
</button>
</div>
</div>
</div>
)}
<div className="dashboard-content">
<div className="career-suggestions-container">
<div
className="career-suggestions-header"
style={{
display: 'flex',
alignItems: 'center',
marginBottom: '15px',
justifyContent: 'center',
gap: '15px'
}}
>
<label>
Preparation Level:
<select
value={selectedJobZone}
onChange={(e) => setSelectedJobZone(Number(e.target.value))}
style={{ marginLeft: '5px', padding: '2px', width: '200px' }}
>
<option value="">All Preparation Levels</option>
{Object.entries(jobZoneLabels).map(([zone, label]) => (
<option key={zone} value={zone}>{label}</option>
))}
</select>
</label>
<label>
Fit:
<select
value={selectedFit}
onChange={(e) => setSelectedFit(e.target.value)}
style={{ marginLeft: '5px', padding: '2px', width: '150px' }}
>
<option value="">All Fit Levels</option>
{Object.entries(fitLabels).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</label>
</div>
<CareerSuggestions
careerSuggestions={memoizedCareerSuggestions}
onCareerClick={handleCareerClick}
/>
</div>
<div className="riasec-container">
<div className="riasec-scores">
<h2>RIASEC Scores</h2>
<Bar data={chartData} />
</div>
<div className="riasec-descriptions">
<h3>RIASEC Personality Descriptions</h3>
{riaSecDescriptions.length > 0 ? (
<ul>
{riaSecDescriptions.map((desc, index) => (
<li key={index}>
<strong>{riaSecScores[index]?.area}:</strong> {desc}
</li>
))}
</ul>
) : (
<p>Loading descriptions...</p>
)}
</div>
</div>
</div>
{memoizedPopoutPanel}
<div className="chatbot-widget">
{careerSuggestions.length > 0 ? (
<Chatbot context={chatbotContext} />
) : (
<p>Loading Chatbot...</p>
)}
</div>
<div
className="data-source-acknowledgment"
style={{
marginTop: '20px',
padding: '10px',
borderTop: '1px solid #ccc',
fontSize: '12px',
color: '#666',
textAlign: 'center'
}}
>
<p>
Career results and RIASEC scores are provided by
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>, in conjunction with the
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer"> Bureau of Labor Statistics</a>, and the
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer"> National Center for Education Statistics (NCES)</a>.
</p>
</div>
</div>
);
}
export default Dashboard;