Distance Calculations
This commit is contained in:
parent
b7461ef06b
commit
2331378114
@ -2,7 +2,7 @@ ONET_USERNAME=aptivaai
|
||||
ONET_PASSWORD=2296ahq
|
||||
|
||||
REACT_APP_BLS_API_KEY=80d7a65a809a43f3a306a41ec874d231
|
||||
GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20
|
||||
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20
|
||||
COLLEGE_SCORECARD_KEY = BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj
|
||||
|
||||
REACT_APP_API_URL=http://localhost:5001
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -13,10 +13,6 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
@ -23,6 +23,8 @@ dotenv.config({ path: envPath });
|
||||
console.log(`Loaded environment variables from: ${envPath}`);
|
||||
console.log('ONET_USERNAME:', process.env.ONET_USERNAME);
|
||||
console.log('ONET_PASSWORD:', process.env.ONET_PASSWORD);
|
||||
console.log('Google Maps API Key:', process.env.GOOGLE_MAPS_API_KEY)
|
||||
|
||||
|
||||
const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'https://dev.aptivaai.com'];
|
||||
const mappingFilePath = '/home/jcoakley/aptiva-dev1-app/public/CIP_to_ONET_SOC.xlsx';
|
||||
@ -49,6 +51,22 @@ const initDB = async () => {
|
||||
// Initialize database before starting the server
|
||||
initDB();
|
||||
|
||||
let userProfileDb;
|
||||
const initUserProfileDb = async () => {
|
||||
try {
|
||||
userProfileDb = await open({
|
||||
filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db', // Path to user_profile.db
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
console.log('Connected to user_profile.db.');
|
||||
} catch (error) {
|
||||
console.error('Error connecting to user_profile.db:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize user_profile.db before starting the server
|
||||
initUserProfileDb();
|
||||
|
||||
// Add security headers using helmet
|
||||
app.use(
|
||||
helmet({
|
||||
@ -158,34 +176,88 @@ app.get('/api/onet/questions', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// New route to handle Google Maps geocoding
|
||||
app.get('/api/maps/distance', async (req, res) => {
|
||||
const { origins, destinations } = req.query;
|
||||
console.log('Query parameters received:', req.query); // Log the entire query object
|
||||
// Helper function to geocode an address or zip code
|
||||
// Function to geocode a ZIP code
|
||||
const geocodeZipCode = async (zipCode) => {
|
||||
try {
|
||||
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(userZipcode)}&key=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20`;
|
||||
console.log('Constructed Geocode URL:', geocodeUrl); // Check if encoding helps
|
||||
|
||||
if (!origins || !destinations) {
|
||||
console.error('Missing parameters:', { origins, destinations });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Origin and destination parameters are required.' });
|
||||
const response = await axios.get(geocodeUrl);
|
||||
|
||||
if (response.data.status === 'OK' && response.data.results.length > 0) {
|
||||
const location = response.data.results[0].geometry.location;
|
||||
return location; // Return the latitude and longitude
|
||||
} else {
|
||||
throw new Error('Geocoding failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error geocoding ZIP code:', error.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
app.post('/api/maps/distance', async (req, res) => {
|
||||
const { userZipcode, destinations } = req.body;
|
||||
|
||||
if (!userZipcode || !destinations) {
|
||||
return res.status(400).json({ error: 'User ZIP code and destination address are required.' });
|
||||
}
|
||||
|
||||
const apiKey = process.env.GOOGLE_MAPS_API_KEY; // Use the Google Maps API key from the environment variable
|
||||
const distanceUrl = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent(
|
||||
origins
|
||||
)}&destinations=${encodeURIComponent(destinations)}&units=imperial&key=${apiKey}`;
|
||||
console.log('Constructed Distance Matrix API URL:', distanceUrl);
|
||||
|
||||
|
||||
try {
|
||||
const response = await axios.get(distanceUrl);
|
||||
res.status(200).json(response.data);
|
||||
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY; // Get the API key
|
||||
|
||||
// Geocode the user's ZIP code
|
||||
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${userZipcode}&components=country:US&key=${googleMapsApiKey}`;
|
||||
const geocodeResponse = await axios.get(geocodeUrl);
|
||||
|
||||
if (geocodeResponse.data.status === 'OK' && geocodeResponse.data.results.length > 0) {
|
||||
const userLocation = geocodeResponse.data.results[0].geometry.location; // Get lat, lng
|
||||
const origins = `${userLocation.lat},${userLocation.lng}`; // User's location as lat/lng
|
||||
|
||||
// Call the Distance Matrix API using the geocoded user location and school address
|
||||
const distanceUrl = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${origins}&destinations=${encodeURIComponent(destinations)}&units=imperial&key=${googleMapsApiKey}`;
|
||||
const distanceResponse = await axios.get(distanceUrl);
|
||||
|
||||
if (distanceResponse.data.status !== 'OK') {
|
||||
return res.status(500).json({ error: 'Error fetching distance from Google Maps API' });
|
||||
}
|
||||
|
||||
const { distance, duration } = distanceResponse.data.rows[0].elements[0];
|
||||
res.json({ distance: distance.text, duration: duration.text });
|
||||
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Unable to geocode user ZIP code.' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching distance data:', error.message);
|
||||
res.status(500).json({ error: 'Failed to fetch distance data', details: error.message });
|
||||
console.error('Error during distance calculation:', error);
|
||||
res.status(500).json({ error: 'Internal server error', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Route to fetch user profile by ID including ZIP code
|
||||
app.get('/api/user-profile/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: 'Profile ID is required' });
|
||||
}
|
||||
|
||||
const query = `SELECT area, zipcode FROM user_profile WHERE id = ?`;
|
||||
db.get(query, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error('Error fetching user profile:', err.message);
|
||||
return res.status(500).json({ error: 'Failed to fetch user profile' });
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: 'Profile not found' });
|
||||
}
|
||||
|
||||
res.json({ area: row.area, zipcode: row.zipcode });
|
||||
});
|
||||
});
|
||||
|
||||
// Load the economic projections data from the Excel file
|
||||
const projectionsFilePath = path.resolve(__dirname, '..', 'public', 'occprj.xlsx'); // Adjusted path
|
||||
@ -498,7 +570,7 @@ app.get('/api/user-profile/:id', (req, res) => {
|
||||
return res.status(400).json({ error: 'Profile ID is required' });
|
||||
}
|
||||
|
||||
const query = `SELECT area FROM user_profile WHERE id = ?`;
|
||||
const query = `SELECT area, zipcode FROM user_profile WHERE id = ?`;
|
||||
db.get(query, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error('Error fetching user profile:', err.message);
|
||||
@ -509,7 +581,7 @@ app.get('/api/user-profile/:id', (req, res) => {
|
||||
return res.status(404).json({ error: 'Profile not found' });
|
||||
}
|
||||
|
||||
res.json({ area: row.area });
|
||||
res.json({ area: row.area, zipcode: row.zipcode });
|
||||
});
|
||||
});
|
||||
|
||||
|
8
package-lock.json
generated
8
package-lock.json
generated
@ -16378,16 +16378,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
|
BIN
public/ltprojections.xlsx
Normal file
BIN
public/ltprojections.xlsx
Normal file
Binary file not shown.
@ -9,22 +9,19 @@ export function CareerSuggestions({ careerSuggestions = [], onCareerClick }) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Career Suggestions</h2>
|
||||
<ul>
|
||||
{careerSuggestions.map((career, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
<div className="career-suggestions-grid">
|
||||
{careerSuggestions.map((career) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('Button clicked for:', career);
|
||||
onCareerClick(career);
|
||||
}}
|
||||
key={career.code}
|
||||
className="career-button"
|
||||
onClick={() => onCareerClick(career)}
|
||||
>
|
||||
{career.title || `Career ${index + 1}`}
|
||||
{career.title}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default CareerSuggestions;
|
@ -1,13 +1,20 @@
|
||||
/* Dashboard.css */
|
||||
|
||||
/* Main Dashboard Layout */
|
||||
.dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Ensure responsive layout */
|
||||
grid-template-columns: 1fr 2fr; /* Two columns: careers on the left, RIASEC on the right */
|
||||
gap: 20px;
|
||||
min-height: 100vh; /* Full height */
|
||||
padding: 20px;
|
||||
background-color: #f4f7fa;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
flex-grow: 1; /* Push acknowledgment to the bottom */
|
||||
}
|
||||
|
||||
/* Sections in Dashboard */
|
||||
.dashboard section {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
@ -22,6 +29,7 @@
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
h2 {
|
||||
font-size: 1.4rem;
|
||||
color: #2b4a67;
|
||||
@ -30,88 +38,50 @@ h2 {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.career-suggestions h1 {
|
||||
font-size: 1.75rem; /* Adjusted size for better hierarchy */
|
||||
margin-bottom: 15px;
|
||||
color: #2b4a67;
|
||||
}
|
||||
|
||||
.career-list {
|
||||
/* Career Suggestions Grid */
|
||||
.career-suggestions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.career-item {
|
||||
padding: 8px;
|
||||
background-color: #eaf6fb;
|
||||
border: 1px solid #d1e7f0;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
.career-suggestions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* Flexible grid */
|
||||
gap: 10px; /* Even spacing */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.career-item:hover {
|
||||
background-color: #d4edf6;
|
||||
}
|
||||
|
||||
.career-item h3 {
|
||||
font-size: 1.1rem; /* Smaller font for career titles */
|
||||
margin-bottom: 4px;
|
||||
font-weight: bold;
|
||||
color: #34596a;
|
||||
}
|
||||
|
||||
.career-item p {
|
||||
font-size: 0.9rem;
|
||||
margin: 4px 0;
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.riasec-scores h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.sign-out-container {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sign-out-btn {
|
||||
padding: 10px 20px;
|
||||
.career-button {
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
font-size: 1rem;
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
white-space: normal; /* Allow wrapping */
|
||||
}
|
||||
|
||||
.sign-out-btn:hover {
|
||||
.career-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.career-item.selected {
|
||||
background-color: #cce5ff; /* Light blue background when selected */
|
||||
border: 2px solid #0056b3; /* Dark blue border */
|
||||
cursor: pointer;
|
||||
/* RIASEC Scores and Descriptions */
|
||||
.riasec-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.career-item:hover {
|
||||
background-color: #e6f7ff; /* Lighter blue when hovering */
|
||||
.riasec-scores {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.riasec-descriptions {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #f1f8ff;
|
||||
@ -131,3 +101,42 @@ h2 {
|
||||
.riasec-descriptions strong {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* Data Source Acknowledgment */
|
||||
.data-source-acknowledgment {
|
||||
grid-column: span 2; /* Make acknowledgment span both columns */
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ccc;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Chart Container */
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Sign Out Button */
|
||||
.sign-out-container {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sign-out-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.sign-out-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
@ -27,10 +27,12 @@ function Dashboard() {
|
||||
const [error, setError] = useState(null);
|
||||
const [userState, setUserState] = useState(null);
|
||||
const [areaTitle, setAreaTitle] = useState(null);
|
||||
const [userZipcode, setUserZipcode] = useState(null);
|
||||
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
|
||||
|
||||
// Dynamic API URL
|
||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||
const googleMapsApiKey = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
|
||||
|
||||
useEffect(() => {
|
||||
let descriptions = []; // Declare outside for scope accessibility
|
||||
@ -58,10 +60,11 @@ function Dashboard() {
|
||||
const profileData = await profileResponse.json();
|
||||
console.log('Fetched User Profile:', profileData);
|
||||
|
||||
const { state, area } = profileData; // Use 'area' instead of 'AREA_TITLE'
|
||||
const { state, area, zipcode } = profileData; // Use 'area' instead of 'AREA_TITLE'
|
||||
setUserState(state);
|
||||
setAreaTitle(area && area.trim() ? area.trim() : ''); // Ensure 'area' is set correctly
|
||||
console.log('Profile Data Set:', { state, area });
|
||||
setUserZipcode(zipcode); // Set 'zipcode' in the state
|
||||
console.log('Profile Data Set:', { state, area, zipcode });
|
||||
} else {
|
||||
console.error('Failed to fetch user profile');
|
||||
}
|
||||
@ -80,10 +83,10 @@ function Dashboard() {
|
||||
setLoading(true); // Enable loading state only when career is clicked
|
||||
setError(null); // Clear previous errors
|
||||
setCareerDetails({}); // Reset career details to avoid undefined errors
|
||||
setSchools([]);
|
||||
setSalaryData([]);
|
||||
setEconomicProjections({});
|
||||
setTuitionData([]);
|
||||
setSchools([]); // Reset schools
|
||||
setSalaryData([]); // Reset salary data
|
||||
setEconomicProjections({}); // Reset economic projections
|
||||
setTuitionData([]); // Reset tuition data
|
||||
|
||||
if (!socCode) {
|
||||
console.error('SOC Code is missing');
|
||||
@ -98,27 +101,67 @@ function Dashboard() {
|
||||
const { cipCode } = await cipResponse.json();
|
||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||
|
||||
|
||||
|
||||
// Step 2: Fetch Data in Parallel
|
||||
const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
|
||||
fetchSchools(cleanedCipCode, userState),
|
||||
axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`),
|
||||
axios.get(`${apiUrl}/tuition`, { // <-- Removed CIP code from URL path
|
||||
axios.get(`${apiUrl}/tuition`, {
|
||||
params: {
|
||||
cipCode: cleanedCipCode, // Moved to query params
|
||||
cipCode: cleanedCipCode,
|
||||
state: userState
|
||||
},
|
||||
}),
|
||||
axios.get(`${apiUrl}/salary`, {
|
||||
params: {
|
||||
socCode: socCode.split('.')[0],
|
||||
area: areaTitle // Pass the areaTitle directly as it is
|
||||
area: areaTitle
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Step 3: Format Salary Data
|
||||
// Check if `userZipcode` is set correctly
|
||||
const currentUserZipcode = userZipcode;
|
||||
if (!currentUserZipcode) {
|
||||
console.error("User Zipcode is not set correctly:", currentUserZipcode);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Add distance information to each school
|
||||
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
|
||||
// Combine Street Address, City, State, and ZIP to form the full school address
|
||||
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
|
||||
|
||||
if (!currentUserZipcode || !schoolAddress) {
|
||||
console.error('Missing ZIP codes or school address:', { currentUserZipcode, schoolAddress });
|
||||
return { ...school, distance: 'Error', duration: 'Error' };
|
||||
}
|
||||
|
||||
console.log("Calculating distance for User Zipcode:", currentUserZipcode, "and School Address:", schoolAddress);
|
||||
|
||||
try {
|
||||
// Send the user's ZIP code and school address to the backend
|
||||
const response = await axios.post(`${apiUrl}/maps/distance`, {
|
||||
userZipcode: currentUserZipcode, // Pass the ZIP code (or lat/lng) of the user
|
||||
destinations: schoolAddress, // Pass the full school address
|
||||
});
|
||||
|
||||
const { distance, duration } = response.data;
|
||||
return {
|
||||
...school, // Keep all school data
|
||||
distance, // Add the distance value
|
||||
duration, // Add the duration value
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching distance:', error);
|
||||
return {
|
||||
...school,
|
||||
distance: 'Error',
|
||||
duration: 'Error',
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
// Step 4: Format Salary Data
|
||||
const salaryDataPoints = [
|
||||
{ percentile: '10th Percentile', value: salaryResponse.data.A_PCT10 || 0 },
|
||||
{ percentile: '25th Percentile', value: salaryResponse.data.A_PCT25 || 0 },
|
||||
@ -127,12 +170,12 @@ function Dashboard() {
|
||||
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
|
||||
];
|
||||
|
||||
// Step 4: Consolidate Career Details
|
||||
// Step 5: Consolidate Career Details
|
||||
setCareerDetails({
|
||||
...career,
|
||||
economicProjections: economicResponse.data,
|
||||
salaryData: salaryDataPoints,
|
||||
schools: filteredSchools,
|
||||
schools: schoolsWithDistance, // Add schools with distances
|
||||
tuitionData: tuitionResponse.data,
|
||||
});
|
||||
} catch (error) {
|
||||
@ -145,6 +188,7 @@ function Dashboard() {
|
||||
[userState, apiUrl, areaTitle]
|
||||
);
|
||||
|
||||
|
||||
const chartData = {
|
||||
labels: riaSecScores.map((score) => score.area),
|
||||
datasets: [
|
||||
@ -200,6 +244,26 @@ function Dashboard() {
|
||||
userState={userState}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Acknowledgment Section */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -1,67 +1,62 @@
|
||||
// EconomicProjections (in EconomicProjections.js)
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
function EconomicProjections({ socCode }) {
|
||||
const [projections, setProjections] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
console.log(axios.defaults.baseURL); // Check if baseURL is set
|
||||
const apiUrl = process.env.REACT_APP_API_URL || '/api';
|
||||
useEffect(() => {
|
||||
if (socCode) {
|
||||
const cleanedSocCode = socCode.split('.')[0]; // Clean the SOC code inside the component
|
||||
const cleanedSocCode = socCode.split('.')[0]; // Clean the SOC code
|
||||
console.log(`Fetching economic projections for cleaned SOC code: ${cleanedSocCode}`);
|
||||
|
||||
const fetchProjections = async () => {
|
||||
const projectionsUrl = `${apiUrl}/projections/${cleanedSocCode}`;
|
||||
// Log URL and SOC code only in development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(`Fetching projections from: ${projectionsUrl}`);
|
||||
console.log(`Cleaned SOC Code: ${cleanedSocCode}`);
|
||||
}
|
||||
try {
|
||||
const cleanedSocCode = socCode.split('.')[0]; // Clean SOC code
|
||||
const projectionsUrl = `${apiUrl}/projections/${cleanedSocCode}`;
|
||||
// Load the Excel file from public folder
|
||||
const response = await fetch('/public/ltprojections.xlsx');
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||
|
||||
// Log URL and SOC code only in development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(`Fetching projections from: ${projectionsUrl}`);
|
||||
console.log(`Cleaned SOC Code: ${cleanedSocCode}`);
|
||||
}
|
||||
// API call to fetch projections
|
||||
const response = await axios.get(projectionsUrl);
|
||||
// Assuming data is in the first sheet
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const data = XLSX.utils.sheet_to_json(sheet);
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
setProjections(response.data); // Set projections
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('Projections Response:', response.data); // Log data in development
|
||||
}
|
||||
// Find the projections matching the SOC code
|
||||
const filteredProjections = data.find(
|
||||
(row) => row['SOC Code'] === cleanedSocCode
|
||||
);
|
||||
|
||||
if (filteredProjections) {
|
||||
setProjections(filteredProjections);
|
||||
} else {
|
||||
throw new Error('Invalid response from server.');
|
||||
throw new Error('No data found for the given SOC code.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error fetching economic projections.');
|
||||
setError('Error loading economic projections.');
|
||||
console.error('Projections Fetch Error:', err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
fetchProjections();
|
||||
}
|
||||
}, [socCode,apiUrl]); // This runs when the socCode prop changes
|
||||
}, [socCode]);
|
||||
|
||||
if (error) {
|
||||
return <div className="error">{error}</div>;
|
||||
}
|
||||
|
||||
if (!projections) {
|
||||
return <div>Loading economic projections...</div>;
|
||||
if (loading) {
|
||||
return <div className="loading-spinner">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="economic-projections">
|
||||
<h3>Economic Projections for {projections.Occupation || 'Unknown Occupation'}</h3>
|
||||
<h3>Economic Projections for {projections['Occupation Title'] || 'Unknown Occupation'}</h3>
|
||||
<ul>
|
||||
<li>2022 Employment: {projections['2022 Employment']}</li>
|
||||
<li>2032 Employment: {projections['2032 Employment']}</li>
|
||||
|
@ -5,16 +5,15 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
||||
const [interestRate, setInterestRate] = useState(5.5); // Default federal loan interest rate
|
||||
const [loanTerm, setLoanTerm] = useState(10); // Default loan term (10 years)
|
||||
const [extraPayment, setExtraPayment] = useState(0); // Extra monthly payment
|
||||
const [currentSalary, setCurrentSalary] = useState(0); // Current salary input
|
||||
const [results, setResults] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [sortBy, setSortBy] = useState('monthlyPayment'); // Default sort option
|
||||
const [currentSalary, setCurrentSalary] = useState(0); // New state for current salary
|
||||
|
||||
// Validation function
|
||||
const validateInputs = () => {
|
||||
if (!schools || schools.length === 0 || !salaryData || salaryData.length === 0) {
|
||||
setError('School or salary data is missing.');
|
||||
if (!schools || schools.length === 0) {
|
||||
setError('School data is missing. Loan calculations cannot proceed.');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -42,10 +41,10 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Calculate loan details for each school
|
||||
// Loan calculation function for all schools
|
||||
const calculateLoanDetails = () => {
|
||||
if (!validateInputs()) return;
|
||||
setLoading(true);
|
||||
if (!validateInputs()) return; // Validate inputs before calculation
|
||||
|
||||
const schoolResults = schools.map((school) => {
|
||||
const tuition = tuitionType === 'inState' ? school.inState : school.outOfState;
|
||||
const monthlyRate = interestRate / 12 / 100;
|
||||
@ -69,23 +68,36 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
||||
|
||||
const totalLoanCost = extraMonthlyPayment * monthsWithExtra;
|
||||
|
||||
// Handle missing salary data
|
||||
let salary = salaryData && salaryData[0]?.value ? salaryData[0].value : null;
|
||||
let netGain = 'N/A';
|
||||
let monthlySalary = 'N/A';
|
||||
|
||||
if (salary) {
|
||||
// Calculate net gain
|
||||
const salary = salaryData[0].value; // 10th percentile salary as default
|
||||
const totalSalary = salary * earningHorizon;
|
||||
const netGain = totalSalary - totalLoanCost;
|
||||
const currentSalaryEarnings = currentSalary * earningHorizon * Math.pow(1.03, earningHorizon); // 3% growth
|
||||
netGain = (totalSalary - totalLoanCost - currentSalaryEarnings).toFixed(2);
|
||||
|
||||
// Monthly salary
|
||||
monthlySalary = (salary / 12).toFixed(2);
|
||||
}
|
||||
|
||||
return {
|
||||
...school,
|
||||
tuition,
|
||||
monthlyPayment: minimumMonthlyPayment.toFixed(2),
|
||||
totalMonthlyPayment: extraMonthlyPayment.toFixed(2), // Add total payment including extra
|
||||
totalLoanCost: totalLoanCost.toFixed(2),
|
||||
netGain: netGain.toFixed(2),
|
||||
netGain,
|
||||
monthlySalary,
|
||||
};
|
||||
});
|
||||
|
||||
setResults(schoolResults);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Loan Repayment and ROI Analysis</h2>
|
||||
@ -93,45 +105,68 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
||||
<div>
|
||||
<label>
|
||||
Tuition Type:
|
||||
<select value={tuitionType} onChange={(e) => setTuitionType(e.target.value)}>
|
||||
<select
|
||||
value={tuitionType}
|
||||
onChange={(e) => setTuitionType(e.target.value)}
|
||||
>
|
||||
<option value="inState">In-State</option>
|
||||
<option value="outOfState">Out-of-State</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Interest Rate (%):
|
||||
<input type="number" value={interestRate} onChange={(e) => setInterestRate(Number(e.target.value))} />
|
||||
<input
|
||||
type="number"
|
||||
value={interestRate}
|
||||
onChange={(e) => setInterestRate(Number(e.target.value))}
|
||||
onFocus={(e) => e.target.select()}
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Loan Term (Years):
|
||||
<input type="number" value={loanTerm} onChange={(e) => setLoanTerm(Number(e.target.value))} />
|
||||
<input
|
||||
type="number"
|
||||
value={loanTerm}
|
||||
onChange={(e) => setLoanTerm(Number(e.target.value))}
|
||||
onFocus={(e) => e.target.select()}
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Extra Monthly Payment ($):
|
||||
<input
|
||||
type="number"
|
||||
value={extraPayment}
|
||||
placeholder="0" // Placeholder added
|
||||
onChange={(e) => setExtraPayment(e.target.value)}
|
||||
onFocus={(e) => e.target.select()} // Clear field on focus
|
||||
onChange={(e) => setExtraPayment(Number(e.target.value))}
|
||||
onFocus={(e) => e.target.select()}
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Current Salary ($):
|
||||
Current Salary (Gross Annual $):
|
||||
<input
|
||||
type="number"
|
||||
value={currentSalary}
|
||||
placeholder="0" // Placeholder added
|
||||
onChange={(e) => setCurrentSalary(e.target.value)}
|
||||
onFocus={(e) => e.target.select()} // Clear field on focus
|
||||
onFocus={(e) => e.target.select()}
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button onClick={calculateLoanDetails} disabled={loading}>
|
||||
{loading ? 'Calculating...' : 'Calculate'}
|
||||
</button>
|
||||
</div>
|
||||
{/* Results Section */}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
|
||||
{/* Results Display */}
|
||||
{results.length > 0 && (
|
||||
<div>
|
||||
<h3>Comparison by School</h3>
|
||||
@ -140,14 +175,22 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
||||
<h4>{result.schoolName}</h4>
|
||||
<p>Total Tuition: ${result.tuition}</p>
|
||||
<p>Monthly Payment: ${result.monthlyPayment}</p>
|
||||
<p>Total Monthly Payment (with extra): ${result.totalMonthlyPayment}</p>
|
||||
<p>Total Loan Cost: ${result.totalLoanCost}</p>
|
||||
<p>Net Gain: ${result.netGain}</p>
|
||||
<p>Net Gain: {result.netGain}</p>
|
||||
<p>Monthly Salary (Gross): {result.monthlySalary}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Salary Warning */}
|
||||
{!salaryData || salaryData.length === 0 ? (
|
||||
<p style={{ color: 'red' }}>Salary data is not available for this profession. Loan calculations are limited.</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default LoanRepayment;
|
||||
|
@ -2,27 +2,41 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40%;
|
||||
width: 40%; /* Default width for larger screens */
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow: -3px 0 5px rgba(0, 0, 0, 0.3);
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
transform: translateX(0);
|
||||
transform: translateX(0); /* Ensure visibility by default */
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
z-index: 1000; /* Ensures it is above other elements */
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.popout-panel {
|
||||
width: 100%; /* Use full width for smaller screens */
|
||||
height: 100%; /* Cover full height */
|
||||
left: 0; /* Ensure it appears on the left for mobile */
|
||||
right: unset; /* Override right alignment */
|
||||
}
|
||||
}
|
||||
|
||||
/* Close button adjustments for mobile */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
padding: 8px 12px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
z-index: 1001; /* Keep button above the panel */
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
@ -53,3 +67,9 @@
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* Fix for overflow issue */
|
||||
html, body {
|
||||
overflow-x: hidden; /* Prevent horizontal scrolling */
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ function PopoutPanel({
|
||||
}) {
|
||||
console.log('PopoutPanel Props:', { data, loading, error, userState });
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="popout-panel">
|
||||
@ -78,16 +79,18 @@ function PopoutPanel({
|
||||
tuition['INSTNM']?.toLowerCase().trim() === // Corrected field
|
||||
school['INSTNM']?.toLowerCase().trim() // Match institution name
|
||||
);
|
||||
|
||||
console.log('Schools Data in PopoutPanel:', schools);
|
||||
return (
|
||||
<li key={index}>
|
||||
<strong>{school['INSTNM']}</strong> {/* Updated field */}
|
||||
<br />
|
||||
Degree Type: {school['CREDDESC'] || 'N/A'} {/* Updated field */}
|
||||
Degree Type: {school['CREDDESC'] || 'Degree type information is not available for this program'} {/* Updated field */}
|
||||
<br />
|
||||
In-State Tuition: ${school['In_state cost'] || 'N/A'} {/* Updated field */}
|
||||
In-State Tuition: ${school['In_state cost'] || 'Tuition information is not available for this school'} {/* Updated field */}
|
||||
<br />
|
||||
Out-of-State Tuition: ${school['Out_state cost'] || 'N/A'} {/* Updated field */}
|
||||
Out-of-State Tuition: ${school['Out_state cost'] || 'Tuition information is not available for this school'} {/* Updated field */}
|
||||
<br />
|
||||
Distance: {school['distance'] || 'Distance to school not available'} {/* Added Distance field */}
|
||||
<br />
|
||||
Website:
|
||||
<a href={school['Website']} target="_blank"> {/* Updated field */}
|
||||
@ -98,7 +101,7 @@ function PopoutPanel({
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No schools available.</p>
|
||||
<p>No schools of higher education are available for this career path.</p>
|
||||
)}
|
||||
|
||||
|
||||
@ -111,7 +114,7 @@ function PopoutPanel({
|
||||
<li>Total Change: {economicProjections['Total Change'] || 'N/A'}</li>
|
||||
</ul>
|
||||
) : (
|
||||
<p>No economic projections available.</p>
|
||||
<p>No economic projections available for this career path.</p>
|
||||
)}
|
||||
|
||||
{/* Salary Data Points */}
|
||||
@ -136,25 +139,35 @@ function PopoutPanel({
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p>No salary data available.</p>
|
||||
<p>No salary data is available for this career path.</p>
|
||||
)}
|
||||
|
||||
{/* Loan Repayment Analysis */}
|
||||
{tenthPercentileSalary > 0 && (
|
||||
<div className="loan-repayment-analysis">
|
||||
<h3>Loan Repayment Analysis</h3>
|
||||
<LoanRepayment
|
||||
schools={schools.map((school) => {
|
||||
const years = getProgramLength(school['CREDDESC']); // Updated field
|
||||
const years = getProgramLength(school['CREDDESC']);
|
||||
return {
|
||||
schoolName: school['INSTNM'], // Updated field
|
||||
inState: parseFloat(school['In_state cost'] * years) || 0, // Updated field
|
||||
outOfState: parseFloat(school['Out_state cost'] * years) || 0, // Updated field
|
||||
schoolName: school['INSTNM'],
|
||||
inState: parseFloat(school['In_state cost'] * years) || 0,
|
||||
outOfState: parseFloat(school['Out_state cost'] * years) || 0,
|
||||
};
|
||||
})}
|
||||
salaryData={[{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]}
|
||||
salaryData={
|
||||
tenthPercentileSalary > 0
|
||||
? [{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]
|
||||
: []
|
||||
}
|
||||
earningHorizon={10}
|
||||
/>
|
||||
{!tenthPercentileSalary && (
|
||||
<p>
|
||||
<em>Salary data unavailable. Loan details are based on cost alone.</em>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user