Fixed Expected Salary input in LoanRepayment
This commit is contained in:
parent
2331378114
commit
5f4992d4d3
@ -2,8 +2,10 @@ 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
|
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=https://dev1.aptivaai.com/api
|
||||||
REACT_APP_ENV=development
|
REACT_APP_ENV=production
|
||||||
|
REACT_APP_OPENAI_API_KEY=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
|
@ -30,7 +30,7 @@ console.log('Current Working Directory:', process.cwd());
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 5000;
|
const PORT = 5000;
|
||||||
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://dev1.aptivaai.com'];
|
||||||
|
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
@ -170,7 +170,7 @@ app.post('/api/login', (req, res) => {
|
|||||||
return res.status(401).json({ error: 'Invalid username or password' });
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT
|
const { user_id } = row; // This gets the correct user_id from the row object
|
||||||
const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '2h' });
|
const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '2h' });
|
||||||
res.status(200).json({ token });
|
res.status(200).json({ token });
|
||||||
});
|
});
|
||||||
@ -184,33 +184,44 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Both username and password are required' });
|
return res.status(400).json({ error: 'Both username and password are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = `SELECT hashed_password, user_id FROM user_auth WHERE username = ?`;
|
const query = `SELECT user_auth.user_id, user_auth.hashed_password, user_profile.zipcode
|
||||||
|
FROM user_auth
|
||||||
|
LEFT JOIN user_profile ON user_auth.user_id = user_profile.user_id
|
||||||
|
WHERE user_auth.username = ?`;
|
||||||
|
|
||||||
db.get(query, [username], async (err, row) => {
|
db.get(query, [username], async (err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error querying user_auth:', err.message);
|
console.error('Error querying user_auth:', err.message);
|
||||||
return res.status(500).json({ error: 'Failed to query user authentication data' });
|
return res.status(500).json({ error: 'Failed to query user authentication data' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Row data:', row); // Log the result of the query
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return res.status(401).json({ error: 'Invalid username or password' }); // User not found
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Verify password
|
||||||
const isMatch = await bcrypt.compare(password, row.hashed_password);
|
const isMatch = await bcrypt.compare(password, row.hashed_password);
|
||||||
|
if (!isMatch) {
|
||||||
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
|
}
|
||||||
|
|
||||||
if (isMatch) {
|
// Ensure that you're using the correct user_id
|
||||||
const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '1h' });
|
const { user_id, zipcode } = row;
|
||||||
res.status(200).json({ message: 'Login successful', token, userId: row.user_id });
|
|
||||||
} else {
|
console.log('UserID:', user_id);
|
||||||
res.status(401).json({ error: 'Invalid username or password' });
|
console.log('ZIP Code:', zipcode); // Log the ZIP code to ensure it's correct
|
||||||
}
|
|
||||||
} catch (error) {
|
// Send correct token with user_id
|
||||||
console.error('Error comparing passwords:', error.message);
|
const token = jwt.sign({ userId: user_id }, SECRET_KEY, { expiresIn: '2h' });
|
||||||
res.status(500).json({ error: 'Failed to compare passwords' });
|
|
||||||
}
|
// You can optionally return the ZIP code or any other data as part of the response
|
||||||
|
res.status(200).json({ message: 'Login successful', token, userId: user_id, zipcode });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Route to fetch user profile
|
// Route to fetch user profile
|
||||||
app.get('/api/user-profile', (req, res) => {
|
app.get('/api/user-profile', (req, res) => {
|
||||||
const token = req.headers.authorization?.split(' ')[1];
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
|
@ -11,22 +11,25 @@ import sqlite3 from 'sqlite3';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
|
|
||||||
|
// Correct the order of variable initialization
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const rootPath = path.resolve(__dirname, '..'); // Go one level up to the root folder
|
const rootPath = path.resolve(__dirname, '..'); // Go one level up to the root folder
|
||||||
|
|
||||||
const env = process.env.NODE_ENV?.trim() || 'development'; // Default to 'development'
|
const env = process.env.NODE_ENV?.trim() || 'development'; // Default to 'development'
|
||||||
const envPath = path.resolve(rootPath, `.env.${env}`); // Use root directory for .env files
|
const envPath = path.resolve(rootPath, `.env.${env}`); // Use root directory for .env files
|
||||||
|
|
||||||
dotenv.config({ path: envPath });
|
// Load environment variables as soon as the server starts
|
||||||
|
dotenv.config({ path: envPath }); // Ensure this is called at the very top to load all environment variables
|
||||||
|
|
||||||
|
// Logging environment variables for debugging
|
||||||
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)
|
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://dev1.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';
|
||||||
const institutionFilePath = path.resolve(rootPath, 'public/Institution_data.json');
|
const institutionFilePath = path.resolve(rootPath, 'public/Institution_data.json');
|
||||||
|
|
||||||
@ -177,11 +180,16 @@ app.get('/api/onet/questions', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to geocode an address or zip code
|
// Helper function to geocode an address or zip code
|
||||||
// Function to geocode a ZIP code
|
|
||||||
const geocodeZipCode = async (zipCode) => {
|
const geocodeZipCode = async (zipCode) => {
|
||||||
|
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error('Google Maps API Key is missing.');
|
||||||
|
} else {
|
||||||
|
console.log('Google Maps API Key loaded:', apiKey);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(userZipcode)}&key=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20`;
|
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(zipCode)}&components=country:US&key=${apiKey}`;
|
||||||
console.log('Constructed Geocode URL:', geocodeUrl); // Check if encoding helps
|
console.log('Constructed Geocode URL:', geocodeUrl); // Log the geocoding URL for debugging
|
||||||
|
|
||||||
const response = await axios.get(geocodeUrl);
|
const response = await axios.get(geocodeUrl);
|
||||||
|
|
||||||
@ -197,10 +205,12 @@ const geocodeZipCode = async (zipCode) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
app.post('/api/maps/distance', async (req, res) => {
|
app.post('/api/maps/distance', async (req, res) => {
|
||||||
const { userZipcode, destinations } = req.body;
|
const { userZipcode, destinations } = req.body;
|
||||||
|
|
||||||
if (!userZipcode || !destinations) {
|
if (!userZipcode || !destinations) {
|
||||||
|
console.error('Missing required parameters:', { userZipcode, destinations });
|
||||||
return res.status(400).json({ error: 'User ZIP code and destination address are required.' });
|
return res.status(400).json({ error: 'User ZIP code and destination address are required.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,16 +218,19 @@ app.post('/api/maps/distance', async (req, res) => {
|
|||||||
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY; // Get the API key
|
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY; // Get the API key
|
||||||
|
|
||||||
// Geocode the user's ZIP code
|
// Geocode the user's ZIP code
|
||||||
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${userZipcode}&components=country:US&key=${googleMapsApiKey}`;
|
const userLocation = await geocodeZipCode(userZipcode);
|
||||||
const geocodeResponse = await axios.get(geocodeUrl);
|
if (!userLocation) {
|
||||||
|
return res.status(400).json({ error: 'Unable to geocode user ZIP code.' });
|
||||||
|
}
|
||||||
|
|
||||||
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
|
const origins = `${userLocation.lat},${userLocation.lng}`; // User's location as lat/lng
|
||||||
|
|
||||||
|
console.log('Request Payload:', { userZipcode, destinations }); // Log the parameters
|
||||||
|
|
||||||
// Call the Distance Matrix API using the geocoded user location and school address
|
// 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 distanceUrl = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${origins}&destinations=${encodeURIComponent(destinations)}&units=imperial&key=${googleMapsApiKey}`;
|
||||||
const distanceResponse = await axios.get(distanceUrl);
|
const distanceResponse = await axios.get(distanceUrl);
|
||||||
|
console.log('Distance API Request URL:', distanceUrl); // Log the final request URL
|
||||||
|
|
||||||
if (distanceResponse.data.status !== 'OK') {
|
if (distanceResponse.data.status !== 'OK') {
|
||||||
return res.status(500).json({ error: 'Error fetching distance from Google Maps API' });
|
return res.status(500).json({ error: 'Error fetching distance from Google Maps API' });
|
||||||
@ -226,38 +239,12 @@ app.post('/api/maps/distance', async (req, res) => {
|
|||||||
const { distance, duration } = distanceResponse.data.rows[0].elements[0];
|
const { distance, duration } = distanceResponse.data.rows[0].elements[0];
|
||||||
res.json({ distance: distance.text, duration: duration.text });
|
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 during distance calculation:', error);
|
console.error('Error during distance calculation:', error);
|
||||||
res.status(500).json({ error: 'Internal server error', 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
|
||||||
@ -318,7 +305,7 @@ app.post('/api/onet/submit_answers', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// URLs for career suggestions and RIASEC scores
|
// URLs for career suggestions and RIASEC scores
|
||||||
const careerUrl = `https://services.onetcenter.org/ws/mnm/interestprofiler/careers?answers=${answers}`;
|
const careerUrl = `https://services.onetcenter.org/ws/mnm/interestprofiler/careers?answers=${answers}&start=1&end=1000`;
|
||||||
const resultsUrl = `https://services.onetcenter.org/ws/mnm/interestprofiler/results?answers=${answers}`;
|
const resultsUrl = `https://services.onetcenter.org/ws/mnm/interestprofiler/results?answers=${answers}`;
|
||||||
|
|
||||||
// Fetch career suggestions
|
// Fetch career suggestions
|
||||||
@ -333,6 +320,8 @@ app.post('/api/onet/submit_answers', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Career API Response:', JSON.stringify(careerResponse.data, null, 2));
|
||||||
|
|
||||||
// Fetch RIASEC scores
|
// Fetch RIASEC scores
|
||||||
console.log('Fetching RIASEC scores from:', resultsUrl);
|
console.log('Fetching RIASEC scores from:', resultsUrl);
|
||||||
const resultsResponse = await axios.get(resultsUrl, {
|
const resultsResponse = await axios.get(resultsUrl, {
|
||||||
@ -400,7 +389,51 @@ app.get('/api/onet/career-details/:socCode', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Endpoint to fetch career description and tasks from O*Net
|
||||||
|
app.get('/api/onet/career-description/:socCode', async (req, res) => {
|
||||||
|
const { socCode } = req.params;
|
||||||
|
|
||||||
|
if (!socCode) {
|
||||||
|
return res.status(400).json({ error: 'SOC Code is required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch career details using the O*Net API for the given SOC code
|
||||||
|
const response = await axios.get(
|
||||||
|
`https://services.onetcenter.org/ws/mnm/careers/${socCode}`,
|
||||||
|
{
|
||||||
|
auth: {
|
||||||
|
username: process.env.ONET_USERNAME,
|
||||||
|
password: process.env.ONET_PASSWORD,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the response contains the necessary data
|
||||||
|
if (response.data && response.data.title) {
|
||||||
|
const { title, what_they_do, on_the_job } = response.data;
|
||||||
|
|
||||||
|
// Extract the tasks from the 'on_the_job' field
|
||||||
|
const tasks = on_the_job?.task || [];
|
||||||
|
|
||||||
|
// Prepare the data for the frontend
|
||||||
|
const careerOverview = {
|
||||||
|
description: what_they_do || 'No description available',
|
||||||
|
tasks: tasks.length ? tasks : ['No tasks available'],
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(careerOverview);
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'Career not found for the provided SOC code.' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching career description and tasks:', error.message);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch career description and tasks from O*Net.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Route to handle fetching CIP code based on SOC code
|
// Route to handle fetching CIP code based on SOC code
|
||||||
app.get('/api/cip/:socCode', (req, res) => {
|
app.get('/api/cip/:socCode', (req, res) => {
|
||||||
@ -560,6 +593,59 @@ app.get('/api/salary', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route to fetch job zones and check for missing salary data
|
||||||
|
app.post('/api/job-zones', async (req, res) => {
|
||||||
|
const { socCodes } = req.body;
|
||||||
|
|
||||||
|
if (!socCodes || !Array.isArray(socCodes) || socCodes.length === 0) {
|
||||||
|
return res.status(400).json({ error: "SOC Codes are required." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure SOC codes are formatted correctly (no decimals)
|
||||||
|
const formattedSocCodes = socCodes.map(code => {
|
||||||
|
let cleanedCode = code.trim().replace(/\./g, ""); // Remove periods
|
||||||
|
if (!cleanedCode.includes("-") && cleanedCode.length === 6) {
|
||||||
|
cleanedCode = cleanedCode.slice(0, 2) + "-" + cleanedCode.slice(2, 6);
|
||||||
|
}
|
||||||
|
return cleanedCode.slice(0, 7); // Keep first 7 characters
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholders = formattedSocCodes.map(() => "?").join(",");
|
||||||
|
const query = `
|
||||||
|
SELECT OCC_CODE, JOB_ZONE,
|
||||||
|
A_MEDIAN, A_PCT10, A_PCT25, A_PCT75
|
||||||
|
FROM salary_data
|
||||||
|
WHERE OCC_CODE IN (${placeholders})
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rows = await db.all(query, formattedSocCodes);
|
||||||
|
|
||||||
|
// Log what is being retrieved from the database
|
||||||
|
console.log("Salary Data Query Results:", rows);
|
||||||
|
|
||||||
|
// Now process `limited_data` flag
|
||||||
|
const jobZoneMapping = rows.reduce((acc, row) => {
|
||||||
|
// Convert empty fields or NULL to a falsy value
|
||||||
|
const isMissingData = [row.A_MEDIAN, row.A_PCT10, row.A_PCT25, row.A_PCT75]
|
||||||
|
.some(value => value === null || value === '' || value === '#' || value === '*');
|
||||||
|
|
||||||
|
acc[row.OCC_CODE] = {
|
||||||
|
job_zone: row.JOB_ZONE,
|
||||||
|
limited_data: isMissingData ? 1 : 0 // Set limited_data flag correctly
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
console.log("Job Zone & Limited Data Mapping:", jobZoneMapping);
|
||||||
|
res.json(jobZoneMapping);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching job zones:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch job zones." });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Route to fetch user profile by ID
|
// Route to fetch user profile by ID
|
||||||
|
0
backend/user_profile
Normal file
0
backend/user_profile
Normal file
Binary file not shown.
964
package-lock.json
generated
964
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
BIN
salary_info.db
BIN
salary_info.db
Binary file not shown.
@ -10,18 +10,20 @@ export function CareerSuggestions({ careerSuggestions = [], onCareerClick }) {
|
|||||||
<div>
|
<div>
|
||||||
<h2>Career Suggestions</h2>
|
<h2>Career Suggestions</h2>
|
||||||
<div className="career-suggestions-grid">
|
<div className="career-suggestions-grid">
|
||||||
{careerSuggestions.map((career) => (
|
{careerSuggestions.map((career) => {
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={career.code}
|
key={career.code}
|
||||||
className="career-button"
|
className="career-button"
|
||||||
onClick={() => onCareerClick(career)}
|
onClick={() => onCareerClick(career)} // Directly pass the career to onCareerClick
|
||||||
>
|
>
|
||||||
{career.title}
|
{career.title}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CareerSuggestions;
|
export default CareerSuggestions;
|
111
src/components/Chatbot.css
Normal file
111
src/components/Chatbot.css
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/* Chatbot Container */
|
||||||
|
.chatbot-container {
|
||||||
|
background-color: #ffffff; /* Solid white background */
|
||||||
|
border: 1px solid #ccc; /* Light gray border */
|
||||||
|
border-radius: 8px; /* Rounded corners */
|
||||||
|
padding: 15px; /* Inner padding */
|
||||||
|
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
|
||||||
|
width: 350px; /* Chatbot width */
|
||||||
|
position: fixed; /* Floating position */
|
||||||
|
bottom: 20px; /* Distance from bottom */
|
||||||
|
right: 20px; /* Distance from right */
|
||||||
|
z-index: 1000; /* Ensure it appears on top */
|
||||||
|
font-family: Arial, sans-serif; /* Font for consistency */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Messages */
|
||||||
|
.chat-messages {
|
||||||
|
max-height: 300px; /* Limit height for scrolling */
|
||||||
|
overflow-y: auto; /* Enable vertical scrolling */
|
||||||
|
margin-bottom: 10px; /* Space below the messages */
|
||||||
|
padding-right: 10px; /* Prevent text from touching the edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual Message */
|
||||||
|
.message {
|
||||||
|
margin: 5px 0; /* Spacing between messages */
|
||||||
|
padding: 8px 10px; /* Inner padding for readability */
|
||||||
|
border-radius: 6px; /* Rounded message boxes */
|
||||||
|
font-size: 14px; /* Readable font size */
|
||||||
|
line-height: 1.4; /* Comfortable line spacing */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Message */
|
||||||
|
.message.user {
|
||||||
|
align-self: flex-end; /* Align user messages to the right */
|
||||||
|
background-color: #007bff; /* Blue background for user */
|
||||||
|
color: #ffffff; /* White text for contrast */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bot Message */
|
||||||
|
.message.bot {
|
||||||
|
align-self: flex-start; /* Align bot messages to the left */
|
||||||
|
background-color: #f1f1f1; /* Light gray background for bot */
|
||||||
|
color: #333333; /* Dark text for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Indicator */
|
||||||
|
.message.bot.typing {
|
||||||
|
font-style: italic; /* Italic text to indicate typing */
|
||||||
|
color: #666666; /* Subtle color */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Input Form */
|
||||||
|
.chat-input-form {
|
||||||
|
display: flex; /* Arrange input and button side by side */
|
||||||
|
gap: 5px; /* Space between input and button */
|
||||||
|
align-items: center; /* Align input and button vertically */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Field */
|
||||||
|
.chat-input-form input {
|
||||||
|
flex: 1; /* Take up remaining space */
|
||||||
|
padding: 10px; /* Padding inside input */
|
||||||
|
border: 1px solid #ccc; /* Light gray border */
|
||||||
|
border-radius: 5px; /* Rounded corners */
|
||||||
|
font-size: 14px; /* Font size */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Focus */
|
||||||
|
.chat-input-form input:focus {
|
||||||
|
outline: none; /* Remove blue outline */
|
||||||
|
border-color: #007bff; /* Blue border on focus */
|
||||||
|
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* Glow effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send Button */
|
||||||
|
.chat-input-form button {
|
||||||
|
background-color: #007bff; /* Blue background */
|
||||||
|
color: #ffffff; /* White text */
|
||||||
|
border: none; /* No border */
|
||||||
|
padding: 10px 15px; /* Padding inside button */
|
||||||
|
border-radius: 5px; /* Rounded corners */
|
||||||
|
cursor: pointer; /* Pointer cursor on hover */
|
||||||
|
font-size: 14px; /* Font size */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send Button Hover */
|
||||||
|
.chat-input-form button:hover {
|
||||||
|
background-color: #0056b3; /* Darker blue on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send Button Disabled */
|
||||||
|
.chat-input-form button:disabled {
|
||||||
|
background-color: #cccccc; /* Gray background when disabled */
|
||||||
|
cursor: not-allowed; /* Indicate disabled state */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styling for Chat Messages */
|
||||||
|
.chat-messages::-webkit-scrollbar {
|
||||||
|
width: 8px; /* Width of scrollbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb {
|
||||||
|
background: #cccccc; /* Gray scrollbar thumb */
|
||||||
|
border-radius: 4px; /* Rounded scrollbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #aaaaaa; /* Darker gray on hover */
|
||||||
|
}
|
||||||
|
|
115
src/components/Chatbot.js
Normal file
115
src/components/Chatbot.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import "./Chatbot.css";
|
||||||
|
|
||||||
|
const Chatbot = ({ context }) => {
|
||||||
|
const [messages, setMessages] = useState([
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
"Hi! I’m here to help you with career suggestions, ROI analysis, and any questions you have about your career. How can I assist you today?",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const sendMessage = async (content) => {
|
||||||
|
const userMessage = { role: "user", content };
|
||||||
|
|
||||||
|
// Ensure career data is injected into every API call
|
||||||
|
const contextSummary = `
|
||||||
|
You are an advanced AI career advisor for AptivaAI.
|
||||||
|
Your role is to not only provide career suggestions but to analyze them based on salary potential, job stability, education costs, and market trends.
|
||||||
|
|
||||||
|
Use the following user-specific data:
|
||||||
|
- Career Suggestions: ${context.careerSuggestions.map((c) => c.title).join(", ") || "No suggestions available."}
|
||||||
|
- Selected Career: ${context.selectedCareer?.title || "None"}
|
||||||
|
- Schools: ${context.schools.map((s) => s["INSTNM"]).join(", ") || "No schools available."}
|
||||||
|
- Median Salary: ${
|
||||||
|
context.salaryData.find((s) => s.percentile === "Median")?.value || "Unavailable"
|
||||||
|
}
|
||||||
|
- ROI (Return on Investment): If available, use education costs vs. salary potential to guide users.
|
||||||
|
|
||||||
|
**Your response should ALWAYS provide analysis, not just list careers.**
|
||||||
|
Example responses:
|
||||||
|
- "If you're looking for a high salary right away, X might be a great option, but it has slow growth."
|
||||||
|
- "If you prefer job stability, Y is projected to grow in demand over the next 10 years."
|
||||||
|
- "If work-life balance is a priority, avoid Z as it has high stress and irregular hours."
|
||||||
|
|
||||||
|
If the user asks about "the best career," do not assume a single best choice. Instead, explain trade-offs like:
|
||||||
|
- "If you want high pay now, X is great, but it has limited upward growth."
|
||||||
|
- "If you prefer stability, Y is a better long-term bet."
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const messagesToSend = [
|
||||||
|
{ role: "system", content: contextSummary }, // Inject AptivaAI data on every request
|
||||||
|
...messages,
|
||||||
|
userMessage,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.post(
|
||||||
|
"https://api.openai.com/v1/chat/completions",
|
||||||
|
{
|
||||||
|
model: "gpt-3.5-turbo",
|
||||||
|
messages: messagesToSend,
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const botMessage = response.data.choices[0].message;
|
||||||
|
setMessages([...messages, userMessage, botMessage]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Chatbot Error:", error);
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
userMessage,
|
||||||
|
{ role: "assistant", content: "Error: Unable to fetch response. Please try again." },
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setInput("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (input.trim()) {
|
||||||
|
sendMessage(input.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chatbot-container">
|
||||||
|
<div className="chat-messages">
|
||||||
|
{messages.map((msg, index) => (
|
||||||
|
<div key={index} className={`message ${msg.role}`}>
|
||||||
|
{msg.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{loading && <div className="message assistant">Typing...</div>}
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSubmit} className="chat-input-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Ask a question..."
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={loading}>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Chatbot;
|
@ -1,17 +1,24 @@
|
|||||||
/* Dashboard.css */
|
/* Dashboard.css */
|
||||||
|
|
||||||
/* Main Dashboard Layout */
|
/* Main Dashboard Layout */
|
||||||
|
|
||||||
.dashboard {
|
.dashboard {
|
||||||
display: grid;
|
min-height: 100vh;
|
||||||
grid-template-columns: 1fr 2fr; /* Two columns: careers on the left, RIASEC on the right */
|
|
||||||
gap: 20px;
|
|
||||||
min-height: 100vh; /* Full height */
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f4f7fa;
|
background-color: #f4f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Main Content Layout: Career Suggestions + RIASEC Scores */
|
||||||
.dashboard-content {
|
.dashboard-content {
|
||||||
flex-grow: 1; /* Push acknowledgment to the bottom */
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap; /* This allows the elements to wrap when screen size is small */
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: auto; /* Prevents extra spacing */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sections in Dashboard */
|
/* Sections in Dashboard */
|
||||||
@ -24,6 +31,12 @@
|
|||||||
border-left: 4px solid #6a9fb5;
|
border-left: 4px solid #6a9fb5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.career-button.disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: #666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard section:hover {
|
.dashboard section:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
||||||
@ -38,42 +51,104 @@ h2 {
|
|||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Career Suggestions Grid */
|
/* Career Suggestions Section */
|
||||||
.career-suggestions-container {
|
.career-suggestions-container {
|
||||||
display: flex;
|
flex: 1.5; /* Ensures it takes the majority of space */
|
||||||
flex-direction: column;
|
width: 60%;
|
||||||
gap: 10px;
|
max-width: 75%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Career Suggestions Grid */
|
||||||
.career-suggestions-grid {
|
.career-suggestions-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* Flexible grid */
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
gap: 10px; /* Even spacing */
|
gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Career Buttons */
|
||||||
.career-button {
|
.career-button {
|
||||||
padding: 10px;
|
padding: 8px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: normal; /* Allow wrapping */
|
white-space: normal;
|
||||||
|
border-radius: 3px; /* Less rounded */
|
||||||
|
}
|
||||||
|
|
||||||
|
.career-button.warning {
|
||||||
|
border: 2px solid black; /* Example warning border */
|
||||||
|
background-color: #f8d7da; /* Example background color */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning Icon */
|
||||||
|
.warning-icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: yellow; /* Yellow to indicate limited data */
|
||||||
}
|
}
|
||||||
|
|
||||||
.career-button:hover {
|
.career-button:hover {
|
||||||
background-color: #0056b3;
|
background-color: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RIASEC Scores and Descriptions */
|
|
||||||
|
/* RIASEC Section */
|
||||||
.riasec-container {
|
.riasec-container {
|
||||||
display: flex;
|
flex: 1;
|
||||||
flex-direction: column;
|
max-width: 400px; /* Ensure it stays visible */
|
||||||
gap: 20px;
|
min-width: 350px;
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Filter Container */
|
||||||
|
.filter-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px; /* Ensures alignment */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style Dropdown */
|
||||||
|
.filter-container label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-container select {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RIASEC Scores */
|
||||||
.riasec-scores {
|
.riasec-scores {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -102,15 +177,19 @@ h2 {
|
|||||||
color: #007bff;
|
color: #007bff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Data Source Acknowledgment */
|
/* Acknowledgment Section - Move to Bottom */
|
||||||
.data-source-acknowledgment {
|
.data-source-acknowledgment {
|
||||||
grid-column: span 2; /* Make acknowledgment span both columns */
|
width: 100%;
|
||||||
margin-top: 20px;
|
text-align: center;
|
||||||
padding: 10px;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
text-align: center;
|
padding: 10px;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chart Container */
|
/* Chart Container */
|
||||||
@ -119,8 +198,18 @@ h2 {
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
width: 100%; /* Full width */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chatbot-widget {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Sign Out Button */
|
/* Sign Out Button */
|
||||||
.sign-out-container {
|
.sign-out-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -140,3 +229,34 @@ h2 {
|
|||||||
.sign-out-btn:hover {
|
.sign-out-btn:hover {
|
||||||
background-color: #0056b3;
|
background-color: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Responsive Tweaks */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
flex-direction: column; /* Stacks sections on smaller screens */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-content {
|
||||||
|
flex-direction: column; /* Stacks elements on smaller screens */
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-container {
|
||||||
|
width: 100%; /* Full width on mobile */
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-container select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.riasec-container {
|
||||||
|
max-width: 100%; /* Full width on mobile */
|
||||||
|
position: relative;
|
||||||
|
top: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.career-suggestions-container {
|
||||||
|
max-width: 100%; /* Full width */
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,8 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
||||||
import { CareerSuggestions } from './CareerSuggestions.js';
|
import { CareerSuggestions } from './CareerSuggestions.js';
|
||||||
import PopoutPanel from './PopoutPanel.js';
|
import PopoutPanel from './PopoutPanel.js';
|
||||||
|
import './PopoutPanel.css';
|
||||||
|
import Chatbot from "./Chatbot.js";
|
||||||
import { Bar } from 'react-chartjs-2';
|
import { Bar } from 'react-chartjs-2';
|
||||||
import { fetchSchools } from '../utils/apiUtils.js';
|
import { fetchSchools } from '../utils/apiUtils.js';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
@ -29,10 +31,57 @@ function Dashboard() {
|
|||||||
const [areaTitle, setAreaTitle] = useState(null);
|
const [areaTitle, setAreaTitle] = useState(null);
|
||||||
const [userZipcode, setUserZipcode] = useState(null);
|
const [userZipcode, setUserZipcode] = useState(null);
|
||||||
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
|
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
|
||||||
|
const [selectedJobZone, setSelectedJobZone] = useState('');
|
||||||
|
const [careersWithJobZone, setCareersWithJobZone] = useState([]); // Store careers with job zone info
|
||||||
|
|
||||||
|
const jobZoneLabels = {
|
||||||
|
'1': 'Little or No Preparation',
|
||||||
|
'2': 'Some Preparation Needed',
|
||||||
|
'3': 'Medium Preparation Needed',
|
||||||
|
'4': 'Considerable Preparation Needed',
|
||||||
|
'5': 'Extensive Preparation Needed'
|
||||||
|
};
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
// Fetch job zone mappings after career suggestions are loaded
|
||||||
|
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, // Extract correct value
|
||||||
|
}));
|
||||||
|
|
||||||
|
setCareersWithJobZone(updatedCareers); // Update state
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching job zone information:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchJobZones();
|
||||||
|
}, [careerSuggestions, apiUrl]);
|
||||||
|
|
||||||
|
const filteredCareers = selectedJobZone
|
||||||
|
? careersWithJobZone.filter(career => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
career.job_zone !== null &&
|
||||||
|
career.job_zone !== undefined &&
|
||||||
|
typeof career.job_zone === 'number' &&
|
||||||
|
Number(career.job_zone) === Number(selectedJobZone)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: careersWithJobZone;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let descriptions = []; // Declare outside for scope accessibility
|
let descriptions = []; // Declare outside for scope accessibility
|
||||||
@ -101,67 +150,31 @@ function Dashboard() {
|
|||||||
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
|
// Step 2: Fetch Job Description and Tasks
|
||||||
|
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||||
|
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
||||||
|
const { description, tasks } = await jobDetailsResponse.json();
|
||||||
|
|
||||||
|
// Step 3: Fetch Data in Parallel for other career details
|
||||||
const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
|
const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
|
||||||
fetchSchools(cleanedCipCode, userState),
|
fetchSchools(cleanedCipCode, userState),
|
||||||
axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`),
|
axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`),
|
||||||
axios.get(`${apiUrl}/tuition`, {
|
axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState }}),
|
||||||
params: {
|
axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle }}),
|
||||||
cipCode: cleanedCipCode,
|
|
||||||
state: userState
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
axios.get(`${apiUrl}/salary`, {
|
|
||||||
params: {
|
|
||||||
socCode: socCode.split('.')[0],
|
|
||||||
area: areaTitle
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if `userZipcode` is set correctly
|
// Handle Distance Calculation
|
||||||
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) => {
|
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}`;
|
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`, {
|
const response = await axios.post(`${apiUrl}/maps/distance`, {
|
||||||
userZipcode: currentUserZipcode, // Pass the ZIP code (or lat/lng) of the user
|
userZipcode,
|
||||||
destinations: schoolAddress, // Pass the full school address
|
destinations: schoolAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { distance, duration } = response.data;
|
const { distance, duration } = response.data;
|
||||||
return {
|
return { ...school, distance, duration };
|
||||||
...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
|
// Process 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 },
|
||||||
@ -170,12 +183,14 @@ function Dashboard() {
|
|||||||
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
|
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Step 5: Consolidate Career Details
|
// Consolidate Career Details with Job Description and Tasks
|
||||||
setCareerDetails({
|
setCareerDetails({
|
||||||
...career,
|
...career,
|
||||||
|
jobDescription: description,
|
||||||
|
tasks: tasks,
|
||||||
economicProjections: economicResponse.data,
|
economicProjections: economicResponse.data,
|
||||||
salaryData: salaryDataPoints,
|
salaryData: salaryDataPoints,
|
||||||
schools: schoolsWithDistance, // Add schools with distances
|
schools: schoolsWithDistance,
|
||||||
tuitionData: tuitionResponse.data,
|
tuitionData: tuitionResponse.data,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -185,10 +200,9 @@ function Dashboard() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[userState, apiUrl, areaTitle]
|
[userState, apiUrl, areaTitle, userZipcode]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const chartData = {
|
const chartData = {
|
||||||
labels: riaSecScores.map((score) => score.area),
|
labels: riaSecScores.map((score) => score.area),
|
||||||
datasets: [
|
datasets: [
|
||||||
@ -204,17 +218,30 @@ function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
<div className="career-suggestions-container">
|
<div className="filter-container">
|
||||||
<CareerSuggestions careerSuggestions={careerSuggestions} onCareerClick={handleCareerClick} />
|
<label htmlFor="preparation-filter">Filter by Preparation Level:</label>
|
||||||
|
<select
|
||||||
|
id="preparation-filter"
|
||||||
|
value={selectedJobZone}
|
||||||
|
onChange={(e) => setSelectedJobZone(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value="">All Preparation Levels</option>
|
||||||
|
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||||
|
<option key={zone} value={zone}>{label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dashboard-content">
|
||||||
|
<div className="career-suggestions-container">
|
||||||
|
<CareerSuggestions careerSuggestions={filteredCareers} onCareerClick={handleCareerClick} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right RIASEC Chart + Descriptions */}
|
|
||||||
<div className="riasec-container">
|
<div className="riasec-container">
|
||||||
<div className="riasec-scores">
|
<div className="riasec-scores">
|
||||||
<h2>RIASEC Scores</h2>
|
<h2>RIASEC Scores</h2>
|
||||||
<Bar data={chartData} />
|
<Bar data={chartData} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="riasec-descriptions">
|
<div className="riasec-descriptions">
|
||||||
<h3>RIASEC Personality Descriptions</h3>
|
<h3>RIASEC Personality Descriptions</h3>
|
||||||
{riaSecDescriptions.length > 0 ? (
|
{riaSecDescriptions.length > 0 ? (
|
||||||
@ -230,6 +257,8 @@ function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{selectedCareer && (
|
{selectedCareer && (
|
||||||
<PopoutPanel
|
<PopoutPanel
|
||||||
@ -245,6 +274,25 @@ function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Pass context to Chatbot */}
|
||||||
|
<div className="chatbot-widget">
|
||||||
|
<Chatbot
|
||||||
|
context={{
|
||||||
|
careerSuggestions,
|
||||||
|
riaSecScores,
|
||||||
|
selectedCareer,
|
||||||
|
schools,
|
||||||
|
salaryData,
|
||||||
|
economicProjections,
|
||||||
|
tuitionData,
|
||||||
|
userState,
|
||||||
|
areaTitle,
|
||||||
|
userZipcode,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Acknowledgment Section */}
|
{/* Acknowledgment Section */}
|
||||||
<div
|
<div
|
||||||
className="data-source-acknowledgment"
|
className="data-source-acknowledgment"
|
||||||
@ -257,6 +305,24 @@ function Dashboard() {
|
|||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<div className="chatbot-widget">
|
||||||
|
<Chatbot
|
||||||
|
context={{
|
||||||
|
careerSuggestions,
|
||||||
|
riaSecScores,
|
||||||
|
selectedCareer,
|
||||||
|
schools,
|
||||||
|
salaryData,
|
||||||
|
economicProjections,
|
||||||
|
tuitionData,
|
||||||
|
userState,
|
||||||
|
areaTitle,
|
||||||
|
userZipcode,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Career results and RIASEC scores are provided by
|
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.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>, in conjunction with the
|
||||||
|
134
src/components/LoanRepayment.css
Normal file
134
src/components/LoanRepayment.css
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
.loan-repayment-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields input,
|
||||||
|
.loan-repayment-fields select {
|
||||||
|
height: 40px; /* Set a consistent height */
|
||||||
|
padding: 0 10px; /* Add horizontal padding */
|
||||||
|
font-size: 1rem; /* Consistent font size */
|
||||||
|
width: 100%; /* Make inputs span full width */
|
||||||
|
box-sizing: border-box; /* Include padding in total width */
|
||||||
|
margin-bottom: 15px; /* Space between fields */
|
||||||
|
border: 1px solid #ccc; /* Light border for all fields */
|
||||||
|
border-radius: 8px; /* Rounded corners */
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields button {
|
||||||
|
width: 100%; /* Full width for button */
|
||||||
|
padding: 12px;
|
||||||
|
height: 45px;
|
||||||
|
margin-top: 10px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center; /* Horizontally center text */
|
||||||
|
align-items: center; /* Vertically center text */
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the container for the fields more clean */
|
||||||
|
.loan-repayment-fields {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto; /* Center the form */
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Adjust margins for form fields */
|
||||||
|
.loan-repayment-fields input,
|
||||||
|
.loan-repayment-fields select,
|
||||||
|
.loan-repayment-fields button {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the heading of the Loan Repayment Analysis is centered */
|
||||||
|
.loan-repayment-fields h3 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: right; /* Centers the button text */
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); /* Limit the column size to a minimum of 280px */
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px; /* Limit the maximum width of the grid */
|
||||||
|
margin: 0 auto; /* Center the grid horizontally */
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container h3 {
|
||||||
|
grid-column: span 3; /* Ensure the header spans across the entire grid */
|
||||||
|
text-align: center; /* Align the text to the center */
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-result-card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: grid;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-result-card h4 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-result-card p {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-result-card .net-gain.positive {
|
||||||
|
color: #2ecc71; /* Green color for positive values */
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-result-card .net-gain.negative {
|
||||||
|
color: #e74c3c; /* Red color for negative values */
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
@ -1,60 +1,57 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import './LoanRepayment.css';
|
||||||
|
|
||||||
function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
function LoanRepayment({
|
||||||
const [tuitionType, setTuitionType] = useState('inState'); // 'inState' or 'outOfState'
|
schools,
|
||||||
const [interestRate, setInterestRate] = useState(5.5); // Default federal loan interest rate
|
salaryData,
|
||||||
const [loanTerm, setLoanTerm] = useState(10); // Default loan term (10 years)
|
setResults,
|
||||||
|
setLoading,
|
||||||
|
}) {
|
||||||
|
const [selectedSalary, setSelectedSalary] = useState('10th Percentile');
|
||||||
|
const [tuitionType, setTuitionType] = useState('inState'); // Tuition type: inState or outOfState
|
||||||
|
const [interestRate, setInterestRate] = useState(5.5); // Interest rate
|
||||||
|
const [loanTerm, setLoanTerm] = useState(10); // Loan term in 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 [currentSalary, setCurrentSalary] = useState(0); // Current salary input
|
||||||
const [results, setResults] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// Validation function
|
|
||||||
const validateInputs = () => {
|
const validateInputs = () => {
|
||||||
if (!schools || schools.length === 0) {
|
if (!schools || schools.length === 0) {
|
||||||
setError('School data is missing. Loan calculations cannot proceed.');
|
setError('School data is missing. Loan calculations cannot proceed.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interestRate <= 0) {
|
if (interestRate <= 0) {
|
||||||
setError('Interest rate must be greater than 0.');
|
setError('Interest rate must be greater than 0.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loanTerm <= 0) {
|
if (loanTerm <= 0) {
|
||||||
setError('Loan term must be greater than 0.');
|
setError('Loan term must be greater than 0.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extraPayment < 0) {
|
if (extraPayment < 0) {
|
||||||
setError('Extra monthly payment cannot be negative.');
|
setError('Extra monthly payment cannot be negative.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSalary < 0) {
|
if (currentSalary < 0) {
|
||||||
setError('Current salary cannot be negative.');
|
setError('Current salary cannot be negative.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
setError(null);
|
||||||
setError(null); // Clear errors if valid
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Loan calculation function for all schools
|
|
||||||
const calculateLoanDetails = () => {
|
const calculateLoanDetails = () => {
|
||||||
if (!validateInputs()) return; // Validate inputs before calculation
|
if (!validateInputs()) return;
|
||||||
|
|
||||||
|
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;
|
||||||
const loanTermMonths = loanTerm * 12;
|
const loanTermMonths = loanTerm * 12;
|
||||||
|
|
||||||
// Calculate minimum monthly payment
|
|
||||||
const minimumMonthlyPayment = tuition * (monthlyRate * Math.pow(1 + monthlyRate, loanTermMonths)) /
|
const minimumMonthlyPayment = tuition * (monthlyRate * Math.pow(1 + monthlyRate, loanTermMonths)) /
|
||||||
(Math.pow(1 + monthlyRate, loanTermMonths) - 1);
|
(Math.pow(1 + monthlyRate, loanTermMonths) - 1);
|
||||||
|
|
||||||
// Total loan cost with extra payments
|
|
||||||
const extraMonthlyPayment = minimumMonthlyPayment + extraPayment;
|
const extraMonthlyPayment = minimumMonthlyPayment + extraPayment;
|
||||||
let remainingBalance = tuition;
|
let remainingBalance = tuition;
|
||||||
let monthsWithExtra = 0;
|
let monthsWithExtra = 0;
|
||||||
@ -68,18 +65,14 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
|||||||
|
|
||||||
const totalLoanCost = extraMonthlyPayment * monthsWithExtra;
|
const totalLoanCost = extraMonthlyPayment * monthsWithExtra;
|
||||||
|
|
||||||
// Handle missing salary data
|
let salary = salaryData.find((point) => point.percentile === selectedSalary)?.value || 0;
|
||||||
let salary = salaryData && salaryData[0]?.value ? salaryData[0].value : null;
|
|
||||||
let netGain = 'N/A';
|
let netGain = 'N/A';
|
||||||
let monthlySalary = 'N/A';
|
let monthlySalary = 'N/A';
|
||||||
|
|
||||||
if (salary) {
|
if (salary > 0) {
|
||||||
// Calculate net gain
|
const totalSalary = salary * loanTerm;
|
||||||
const totalSalary = salary * earningHorizon;
|
const currentSalaryEarnings = currentSalary * loanTerm * Math.pow(1.03, loanTerm);
|
||||||
const currentSalaryEarnings = currentSalary * earningHorizon * Math.pow(1.03, earningHorizon); // 3% growth
|
|
||||||
netGain = (totalSalary - totalLoanCost - currentSalaryEarnings).toFixed(2);
|
netGain = (totalSalary - totalLoanCost - currentSalaryEarnings).toFixed(2);
|
||||||
|
|
||||||
// Monthly salary
|
|
||||||
monthlySalary = (salary / 12).toFixed(2);
|
monthlySalary = (salary / 12).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +80,7 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
|||||||
...school,
|
...school,
|
||||||
tuition,
|
tuition,
|
||||||
monthlyPayment: minimumMonthlyPayment.toFixed(2),
|
monthlyPayment: minimumMonthlyPayment.toFixed(2),
|
||||||
totalMonthlyPayment: extraMonthlyPayment.toFixed(2), // Add total payment including extra
|
totalMonthlyPayment: extraMonthlyPayment.toFixed(2),
|
||||||
totalLoanCost: totalLoanCost.toFixed(2),
|
totalLoanCost: totalLoanCost.toFixed(2),
|
||||||
netGain,
|
netGain,
|
||||||
monthlySalary,
|
monthlySalary,
|
||||||
@ -95,102 +88,48 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setResults(schoolResults);
|
setResults(schoolResults);
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="loan-repayment-container">
|
||||||
|
<form onSubmit={(e) => { e.preventDefault(); calculateLoanDetails(); }}>
|
||||||
<div>
|
<div>
|
||||||
<h2>Loan Repayment and ROI Analysis</h2>
|
<label>Tuition Type:</label>
|
||||||
|
<select value={tuitionType} onChange={(e) => setTuitionType(e.target.value)}>
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
Tuition Type:
|
|
||||||
<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>
|
|
||||||
Interest Rate (%):
|
|
||||||
<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))}
|
|
||||||
onFocus={(e) => e.target.select()}
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Extra Monthly Payment ($):
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={extraPayment}
|
|
||||||
onChange={(e) => setExtraPayment(Number(e.target.value))}
|
|
||||||
onFocus={(e) => e.target.select()}
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Current Salary (Gross Annual $):
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={currentSalary}
|
|
||||||
onChange={(e) => setCurrentSalary(e.target.value)}
|
|
||||||
onFocus={(e) => e.target.select()}
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button onClick={calculateLoanDetails} disabled={loading}>
|
|
||||||
{loading ? 'Calculating...' : 'Calculate'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
||||||
|
|
||||||
{/* Results Display */}
|
|
||||||
{results.length > 0 && (
|
|
||||||
<div>
|
<div>
|
||||||
<h3>Comparison by School</h3>
|
<label>Interest Rate:</label>
|
||||||
{results.map((result, index) => (
|
<input type="number" value={interestRate} onChange={(e) => setInterestRate(e.target.value)} />
|
||||||
<div key={index}>
|
|
||||||
<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>Monthly Salary (Gross): {result.monthlySalary}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Loan Term (years):</label>
|
||||||
|
<input type="number" value={loanTerm} onChange={(e) => setLoanTerm(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Extra Monthly Payment:</label>
|
||||||
|
<input type="number" value={extraPayment} onChange={(e) => setExtraPayment(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Current Salary:</label>
|
||||||
|
<input type="number" value={currentSalary} onChange={(e) => setCurrentSalary(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Expected Salary:</label>
|
||||||
|
<select value={selectedSalary} onChange={(e) => setSelectedSalary(e.target.value)}>
|
||||||
|
{salaryData.map((point, index) => (
|
||||||
|
<option key={index} value={point.percentile}>{point.percentile}</option>
|
||||||
))}
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<button type="submit">Calculate</button>
|
||||||
|
</form>
|
||||||
{/* Salary Warning */}
|
{error && <div className="error">{error}</div>}
|
||||||
{!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;
|
@ -2,7 +2,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 40%; /* Default width for larger screens */
|
width: 60%; /* Increase 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);
|
||||||
@ -21,9 +21,13 @@
|
|||||||
left: 0; /* Ensure it appears on the left for mobile */
|
left: 0; /* Ensure it appears on the left for mobile */
|
||||||
right: unset; /* Override right alignment */
|
right: unset; /* Override right alignment */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.schools-offering {
|
||||||
|
grid-template-columns: 1fr; /* Single column layout for smaller screens */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Close button adjustments for mobile */
|
/* Close button adjustments for mobile */
|
||||||
.close-btn {
|
.close-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@ -37,39 +41,279 @@
|
|||||||
z-index: 1001; /* Keep button above the panel */
|
z-index: 1001; /* Keep button above the panel */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Job Description and Expected Tasks section */
|
||||||
|
.section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
h3 {
|
.job-description,
|
||||||
|
.expected-tasks {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expected Tasks Styling */
|
||||||
|
.expected-tasks {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
.expected-tasks ul {
|
||||||
list-style: none;
|
list-style-position: inside; /* Move the bullets inside, aligning them with text */
|
||||||
padding: 0;
|
padding-left: 20px; /* Add space between the bullet and the text */
|
||||||
}
|
margin: 0;
|
||||||
|
text-align: left; /* Align the text to the left */
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
.expected-tasks li {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px; /* Space between each task */
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
border-bottom: 1px solid #ddd;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
/* Title and task text styling */
|
||||||
|
.expected-tasks h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 2px solid #ccc;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expected-tasks p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Schools section: Grid layout with clear separation */
|
||||||
|
.schools-offering {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); /* Adjust the minimum size of each column */
|
||||||
|
gap: 20px; /* Space between columns */
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%; /* Ensure it uses full width of its container */
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center; /* Centers grid elements horizontally */
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-schools-message {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
grid-column: 1 / -1; /* Forces the message to span all columns */
|
||||||
|
justify-self: center; /* Centers text horizontally */
|
||||||
|
align-self: center; /* Centers text vertically */
|
||||||
|
font-style: italic; /* Optional: Stylize the message */
|
||||||
|
padding: 20px 0; /* Adds spacing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.schools-offering .school-card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schools-offering .school-card div {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Salary Data Section */
|
||||||
|
.salary-data {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.salary-data table {
|
||||||
|
width: 60%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0 auto 20px; /* This centers the table */
|
||||||
|
}
|
||||||
|
|
||||||
|
.salary-data th, .salary-data td {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.salary-data th {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.salary-data td {
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.salary-data td:last-child {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Economic Projections Section */
|
||||||
|
.economic-projections {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.economic-projections ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
list-style-position: inside;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.economic-projections li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loan Repayment Section Styling */
|
||||||
|
.loan-repayment-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields input,
|
||||||
|
.loan-repayment-fields select {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
height: 45px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
background-color: #007bff;
|
background-color: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 8px 12px;
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
border-radius: 8px;
|
||||||
}
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
button:hover {
|
.loan-repayment-fields button:hover {
|
||||||
background-color: #0056b3;
|
background-color: #45a049;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for overflow issue */
|
.loan-repayment-fields button:disabled {
|
||||||
html, body {
|
background-color: #ccc;
|
||||||
overflow-x: hidden; /* Prevent horizontal scrolling */
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields input,
|
||||||
|
.loan-repayment-fields select,
|
||||||
|
.loan-repayment-fields button {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loan-repayment-fields h3 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: right; /* Centers the button text */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comparison Section Styling */
|
||||||
|
.school-comparison-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); /* Dynamic columns */
|
||||||
|
gap: 20px; /* Space between cards */
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%; /* Ensure it uses full width of its container */
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-comparison {
|
||||||
|
display: grid;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.school-comparison h4 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Title Styling */
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 2px solid #ccc;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1a73e8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for overflow issue */
|
||||||
|
html, body {
|
||||||
|
overflow-x: hidden; /* Prevent horizontal scrolling */
|
||||||
|
}
|
@ -1,17 +1,22 @@
|
|||||||
import { ClipLoader } from 'react-spinners';
|
import { ClipLoader } from 'react-spinners';
|
||||||
import LoanRepayment from './LoanRepayment.js';
|
import LoanRepayment from './LoanRepayment.js';
|
||||||
import './PopoutPanel.css';
|
import './PopoutPanel.css';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
function PopoutPanel({
|
function PopoutPanel({
|
||||||
data = {},
|
data = {},
|
||||||
userState = 'N/A', // Passed explicitly from Dashboard
|
userState = 'N/A', // Passed explicitly from Dashboard
|
||||||
loading = false,
|
loading = false,
|
||||||
error = null,
|
error = null,
|
||||||
closePanel
|
closePanel,
|
||||||
}) {
|
}) {
|
||||||
console.log('PopoutPanel Props:', { data, loading, error, userState });
|
console.log('PopoutPanel Props:', { data, loading, error, userState });
|
||||||
|
|
||||||
|
const [isCalculated, setIsCalculated] = useState(false);
|
||||||
|
const [results, setResults] = useState([]); // Store loan repayment calculation results
|
||||||
|
const [loadingCalculation, setLoadingCalculation] = useState(false);
|
||||||
|
|
||||||
|
// Handle loading state
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="popout-panel">
|
<div className="popout-panel">
|
||||||
@ -22,39 +27,17 @@ function PopoutPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="popout-panel">
|
|
||||||
<button className="close-btn" onClick={closePanel}>X</button>
|
|
||||||
<h2>Error Loading Career Details</h2>
|
|
||||||
<p style={{ color: 'red' }}>{error}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle empty data gracefully
|
|
||||||
if (!data || Object.keys(data).length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="popout-panel">
|
|
||||||
<button onClick={closePanel}>Close</button>
|
|
||||||
<h2>No Career Data Available</h2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safely access nested data with fallbacks
|
// Safely access nested data with fallbacks
|
||||||
const {
|
const {
|
||||||
|
jobDescription = null, // Default to null if not provided
|
||||||
|
tasks = null, // Default to null if not provided
|
||||||
title = 'Career Details',
|
title = 'Career Details',
|
||||||
economicProjections = {},
|
economicProjections = {},
|
||||||
salaryData = [],
|
salaryData = [],
|
||||||
schools = [],
|
schools = [],
|
||||||
tuitionData = []
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const tenthPercentileSalary = salaryData?.find(
|
// Get program length for calculating tuition
|
||||||
(point) => point.percentile === '10th Percentile'
|
|
||||||
)?.value || 0;
|
|
||||||
|
|
||||||
const getProgramLength = (degreeType) => {
|
const getProgramLength = (degreeType) => {
|
||||||
if (degreeType?.includes("Associate")) return 2;
|
if (degreeType?.includes("Associate")) return 2;
|
||||||
if (degreeType?.includes("Bachelor")) return 4;
|
if (degreeType?.includes("Bachelor")) return 4;
|
||||||
@ -69,43 +52,48 @@ function PopoutPanel({
|
|||||||
<button onClick={closePanel}>Close</button>
|
<button onClick={closePanel}>Close</button>
|
||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
|
|
||||||
{/* Schools Offering Programs */}
|
{/* Job Description and Tasks */}
|
||||||
<h3>Schools Offering Programs</h3>
|
<div className="job-description">
|
||||||
{Array.isArray(schools) && schools.length > 0 ? (
|
<h3>Job Description</h3>
|
||||||
|
<p>{jobDescription || 'No description available'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="job-tasks">
|
||||||
|
<h3>Expected Tasks</h3>
|
||||||
|
{tasks && tasks.length > 0 ? (
|
||||||
<ul>
|
<ul>
|
||||||
{schools.map((school, index) => {
|
{tasks.map((task, index) => (
|
||||||
const matchingTuitionData = tuitionData.find(
|
<li key={index}>{task}</li>
|
||||||
(tuition) =>
|
))}
|
||||||
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'] || 'Degree type information is not available for this program'} {/* Updated field */}
|
|
||||||
<br />
|
|
||||||
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'] || '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 */}
|
|
||||||
{school['Website']}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<p>No schools of higher education are available for this career path.</p>
|
<p>No tasks available for this career path.</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Schools Offering Programs Section */}
|
||||||
|
<h3>Schools Offering Programs</h3>
|
||||||
|
<div className="schools-offering">
|
||||||
|
{Array.isArray(schools) && schools.length > 0 ? (
|
||||||
|
schools.map((school, index) => (
|
||||||
|
<div key={index} className="school-card">
|
||||||
|
<div><strong>{school['INSTNM']}</strong></div>
|
||||||
|
<div>Degree Type: {school['CREDDESC'] || 'Degree type information is not available for this program'}</div>
|
||||||
|
<div>In-State Tuition: ${school['In_state cost'] || 'Tuition information is not available for this school'}</div>
|
||||||
|
<div>Out-of-State Tuition: ${school['Out_state cost'] || 'Tuition information is not available for this school'}</div>
|
||||||
|
<div>Distance: {school['distance'] || 'Distance to school not available'}</div>
|
||||||
|
<div>
|
||||||
|
Website: <a href={school['Website']} target="_blank" rel="noopener noreferrer">{school['Website']}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="no-schools-message">No schools of higher education are available in your state for this career path.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Economic Projections */}
|
{/* Economic Projections */}
|
||||||
|
<div className="economic-projections">
|
||||||
<h3>Economic Projections for {userState}</h3>
|
<h3>Economic Projections for {userState}</h3>
|
||||||
{economicProjections && typeof economicProjections === 'object' ? (
|
{economicProjections && typeof economicProjections === 'object' ? (
|
||||||
<ul>
|
<ul>
|
||||||
@ -116,10 +104,12 @@ function PopoutPanel({
|
|||||||
) : (
|
) : (
|
||||||
<p>No economic projections available for this career path.</p>
|
<p>No economic projections available for this career path.</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Salary Data Points */}
|
{/* Salary Data Points */}
|
||||||
|
<div className="salary-data">
|
||||||
<h3>Salary Data</h3>
|
<h3>Salary Data</h3>
|
||||||
{salaryData && salaryData.length > 0 ? (
|
{salaryData.length > 0 ? (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -131,20 +121,20 @@ function PopoutPanel({
|
|||||||
{salaryData.map((point, index) => (
|
{salaryData.map((point, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>{point.percentile}</td>
|
<td>{point.percentile}</td>
|
||||||
<td>
|
<td>{point.value > 0 ? `$${parseInt(point.value, 10).toLocaleString()}` : 'N/A'}</td>
|
||||||
{point.value > 0 ? `$${parseInt(point.value, 10).toLocaleString()}` : 'N/A'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<p>No salary data is available for this career path.</p>
|
<p>Salary data is not available.</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Loan Repayment Analysis */}
|
{/* Loan Repayment Analysis */}
|
||||||
<div className="loan-repayment-analysis">
|
|
||||||
<h3>Loan Repayment Analysis</h3>
|
<h3>Loan Repayment Analysis</h3>
|
||||||
|
|
||||||
|
{/* Loan Repayment Calculation Results */}
|
||||||
<LoanRepayment
|
<LoanRepayment
|
||||||
schools={schools.map((school) => {
|
schools={schools.map((school) => {
|
||||||
const years = getProgramLength(school['CREDDESC']);
|
const years = getProgramLength(school['CREDDESC']);
|
||||||
@ -152,21 +142,33 @@ function PopoutPanel({
|
|||||||
schoolName: school['INSTNM'],
|
schoolName: school['INSTNM'],
|
||||||
inState: parseFloat(school['In_state cost'] * years) || 0,
|
inState: parseFloat(school['In_state cost'] * years) || 0,
|
||||||
outOfState: parseFloat(school['Out_state cost'] * years) || 0,
|
outOfState: parseFloat(school['Out_state cost'] * years) || 0,
|
||||||
|
degreeType: school['CREDDESC'],
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
salaryData={
|
salaryData={salaryData}
|
||||||
tenthPercentileSalary > 0
|
setResults={setResults}
|
||||||
? [{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]
|
setLoading={setLoadingCalculation}
|
||||||
: []
|
|
||||||
}
|
|
||||||
earningHorizon={10}
|
|
||||||
/>
|
/>
|
||||||
{!tenthPercentileSalary && (
|
|
||||||
<p>
|
{/* Results Display */}
|
||||||
<em>Salary data unavailable. Loan details are based on cost alone.</em>
|
{results.length > 0 && (
|
||||||
|
<div className="results-container">
|
||||||
|
<h3>Comparisons by School over the life of the loan - assumes a starting salary in the lowest 10%</h3>
|
||||||
|
{results.map((result, index) => (
|
||||||
|
<div className="school-result-card" key={index}>
|
||||||
|
<h4>{result.schoolName} - {result.degreeType || 'Degree type not available'}</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 className={`net-gain ${parseFloat(result.netGain) < 0 ? 'negative' : 'positive'}`}>
|
||||||
|
Net Gain: {result.netGain}
|
||||||
</p>
|
</p>
|
||||||
)}
|
<p>Monthly Salary (Gross): {result.monthlySalary}</p>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
max-width: 280px; /* Optional max width */
|
max-width: 280px; /* Optional max width */
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -24,7 +24,7 @@ function SignIn({ setIsAuthenticated }) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Make a POST request to the backend for authentication
|
// Make a POST request to the backend for authentication
|
||||||
const response = await fetch('https://dev.aptivaai.com/api/signin', {
|
const response = await fetch('https://dev1.aptivaai.com/api/signin', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -45,7 +45,7 @@ function SignIn({ setIsAuthenticated }) {
|
|||||||
localStorage.setItem('userId', userId);
|
localStorage.setItem('userId', userId);
|
||||||
|
|
||||||
console.log('Token and userId saved in localStorage');
|
console.log('Token and userId saved in localStorage');
|
||||||
|
console.log('SignIn response data:', data);
|
||||||
// Call setIsAuthenticated(true) to update the state
|
// Call setIsAuthenticated(true) to update the state
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
navigate('/getting-started'); // Redirect to GettingStarted after SignIn
|
navigate('/getting-started'); // Redirect to GettingStarted after SignIn
|
||||||
|
@ -3,7 +3,6 @@ import axios from 'axios';
|
|||||||
//fetch areas by state
|
//fetch areas by state
|
||||||
export const fetchAreasByState = async (state) => {
|
export const fetchAreasByState = async (state) => {
|
||||||
try {
|
try {
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
|
||||||
const response = await fetch(`${process.env.REACT_APP_API_URL}/Institution_data.json`);
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/Institution_data.json`);
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
|
102
src/utils/fetchJobZones.js
Normal file
102
src/utils/fetchJobZones.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import sqlite3 from "sqlite3";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
const envFile = process.env.NODE_ENV === "production" ? ".env.production" : ".env.development";
|
||||||
|
dotenv.config({ path: envFile });
|
||||||
|
|
||||||
|
console.log(`🛠️ Loaded environment variables from ${envFile}`);
|
||||||
|
|
||||||
|
// O*Net API Credentials
|
||||||
|
const ONET_USERNAME = process.env.ONET_USERNAME;
|
||||||
|
const ONET_PASSWORD = process.env.ONET_PASSWORD;
|
||||||
|
const BASE_URL_JOB_ZONES = "https://services.onetcenter.org/ws/online/job_zones/";
|
||||||
|
|
||||||
|
if (!ONET_USERNAME || !ONET_PASSWORD) {
|
||||||
|
console.error("❌ O*Net API credentials are missing. Check your .env file.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database Path
|
||||||
|
const DB_PATH = "/home/jcoakley/aptiva-dev1-app/salary_info.db";
|
||||||
|
|
||||||
|
// Connect to SQLite
|
||||||
|
const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READWRITE, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("❌ Error connecting to database:", err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("✅ Connected to salary_info.db");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ** Function to clean SOC codes (keep hyphen, remove decimals) **
|
||||||
|
function formatSOCCode(socCode) {
|
||||||
|
return socCode.split(".")[0]; // Converts "43-5111.00" → "43-5111"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Step: Fetch and Assign Job Zones (Using Formatted SOC Codes) **
|
||||||
|
async function fetchAndAssignJobZones() {
|
||||||
|
for (let zone = 1; zone <= 5; zone++) {
|
||||||
|
console.log(`📡 Fetching Job Zone ${zone}`);
|
||||||
|
|
||||||
|
let url = `${BASE_URL_JOB_ZONES}${zone}?start=1&end=500&sort=name`;
|
||||||
|
let jobZoneOccupations = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(`${ONET_USERNAME}:${ONET_PASSWORD}`).toString("base64")}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`❌ Failed to fetch Job Zone ${zone}: HTTP Error ${response.status}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
jobZoneOccupations = data.occupation || [];
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error fetching Job Zone ${zone}:`, error.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Retrieved ${jobZoneOccupations.length} occupations for Job Zone ${zone}`);
|
||||||
|
|
||||||
|
// ** Use `serialize()` to enforce sequential updates **
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run("BEGIN TRANSACTION"); // Start transaction
|
||||||
|
|
||||||
|
jobZoneOccupations.forEach((job, index) => {
|
||||||
|
const soc_code = formatSOCCode(job.code); // Keep hyphen, remove decimal
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
db.run(
|
||||||
|
"UPDATE salary_data SET JOB_ZONE = ? WHERE OCC_CODE = ?",
|
||||||
|
[zone, soc_code],
|
||||||
|
(err) => {
|
||||||
|
if (err) console.error(`❌ Error updating JOB_ZONE for ${soc_code}:`, err.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, index * 100); // Introduce 100ms delay per record
|
||||||
|
});
|
||||||
|
|
||||||
|
db.run("COMMIT"); // End transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✅ All Job Zones assigned.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Run the Process **
|
||||||
|
async function main() {
|
||||||
|
await fetchAndAssignJobZones();
|
||||||
|
|
||||||
|
db.close(() => console.log("✅ Database connection closed."));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
0
user_profile
Normal file
0
user_profile
Normal file
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user