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