Fixed Expected Salary input in LoanRepayment

This commit is contained in:
Josh 2025-03-03 15:50:46 +00:00
parent 2331378114
commit 5f4992d4d3
22 changed files with 1921 additions and 935 deletions

View File

@ -2,8 +2,10 @@ ONET_USERNAME=aptivaai
ONET_PASSWORD=2296ahq
REACT_APP_BLS_API_KEY=80d7a65a809a43f3a306a41ec874d231
GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20
COLLEGE_SCORECARD_KEY = BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj
REACT_APP_API_URL=http://localhost:5001
REACT_APP_ENV=development
REACT_APP_API_URL=https://dev1.aptivaai.com/api
REACT_APP_ENV=production
REACT_APP_OPENAI_API_KEY=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

View File

@ -30,7 +30,7 @@ console.log('Current Working Directory:', process.cwd());
const app = express();
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.use(bodyParser.json());
@ -170,7 +170,7 @@ app.post('/api/login', (req, res) => {
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' });
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' });
}
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) => {
if (err) {
console.error('Error querying user_auth:', err.message);
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) {
return res.status(401).json({ error: 'Invalid username or password' }); // User not found
return res.status(401).json({ error: 'Invalid username or password' });
}
try {
const isMatch = await bcrypt.compare(password, row.hashed_password);
if (isMatch) {
const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '1h' });
res.status(200).json({ message: 'Login successful', token, userId: row.user_id });
} else {
res.status(401).json({ error: 'Invalid username or password' });
}
} catch (error) {
console.error('Error comparing passwords:', error.message);
res.status(500).json({ error: 'Failed to compare passwords' });
// Verify password
const isMatch = await bcrypt.compare(password, row.hashed_password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid username or password' });
}
// Ensure that you're using the correct user_id
const { user_id, zipcode } = row;
console.log('UserID:', user_id);
console.log('ZIP Code:', zipcode); // Log the ZIP code to ensure it's correct
// Send correct token with user_id
const token = jwt.sign({ userId: user_id }, SECRET_KEY, { expiresIn: '2h' });
// 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
app.get('/api/user-profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];

View File

@ -11,22 +11,25 @@ import sqlite3 from 'sqlite3';
import fs from 'fs';
import readline from 'readline';
// Correct the order of variable initialization
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
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 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('ONET_USERNAME:', process.env.ONET_USERNAME);
console.log('ONET_PASSWORD:', process.env.ONET_PASSWORD);
console.log('Google Maps API Key:', process.env.GOOGLE_MAPS_API_KEY)
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 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
// Function to geocode a ZIP code
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 {
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(userZipcode)}&key=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20`;
console.log('Constructed Geocode URL:', geocodeUrl); // Check if encoding helps
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(zipCode)}&components=country:US&key=${apiKey}`;
console.log('Constructed Geocode URL:', geocodeUrl); // Log the geocoding URL for debugging
const response = await axios.get(geocodeUrl);
@ -197,10 +205,12 @@ const geocodeZipCode = async (zipCode) => {
}
};
app.post('/api/maps/distance', async (req, res) => {
const { userZipcode, destinations } = req.body;
if (!userZipcode || !destinations) {
console.error('Missing required parameters:', { userZipcode, destinations });
return res.status(400).json({ error: 'User ZIP code and destination address are required.' });
}
@ -208,56 +218,33 @@ app.post('/api/maps/distance', async (req, res) => {
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY; // Get the API key
// Geocode the user's ZIP code
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${userZipcode}&components=country:US&key=${googleMapsApiKey}`;
const geocodeResponse = await axios.get(geocodeUrl);
if (geocodeResponse.data.status === 'OK' && geocodeResponse.data.results.length > 0) {
const userLocation = geocodeResponse.data.results[0].geometry.location; // Get lat, lng
const origins = `${userLocation.lat},${userLocation.lng}`; // User's location as lat/lng
// Call the Distance Matrix API using the geocoded user location and school address
const distanceUrl = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${origins}&destinations=${encodeURIComponent(destinations)}&units=imperial&key=${googleMapsApiKey}`;
const distanceResponse = await axios.get(distanceUrl);
if (distanceResponse.data.status !== 'OK') {
return res.status(500).json({ error: 'Error fetching distance from Google Maps API' });
}
const { distance, duration } = distanceResponse.data.rows[0].elements[0];
res.json({ distance: distance.text, duration: duration.text });
} else {
const userLocation = await geocodeZipCode(userZipcode);
if (!userLocation) {
return res.status(400).json({ error: 'Unable to geocode user ZIP code.' });
}
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
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);
console.log('Distance API Request URL:', distanceUrl); // Log the final request URL
if (distanceResponse.data.status !== 'OK') {
return res.status(500).json({ error: 'Error fetching distance from Google Maps API' });
}
const { distance, duration } = distanceResponse.data.rows[0].elements[0];
res.json({ distance: distance.text, duration: duration.text });
} catch (error) {
console.error('Error during distance calculation:', error);
res.status(500).json({ error: 'Internal server error', details: error.message });
}
});
// Route to fetch user profile by ID including ZIP code
app.get('/api/user-profile/:id', (req, res) => {
const { id } = req.params;
if (!id) {
return res.status(400).json({ error: 'Profile ID is required' });
}
const query = `SELECT area, zipcode FROM user_profile WHERE id = ?`;
db.get(query, [id], (err, row) => {
if (err) {
console.error('Error fetching user profile:', err.message);
return res.status(500).json({ error: 'Failed to fetch user profile' });
}
if (!row) {
return res.status(404).json({ error: 'Profile not found' });
}
res.json({ area: row.area, zipcode: row.zipcode });
});
});
// Load the economic projections data from the Excel file
const projectionsFilePath = path.resolve(__dirname, '..', 'public', 'occprj.xlsx'); // Adjusted path
@ -318,7 +305,7 @@ app.post('/api/onet/submit_answers', async (req, res) => {
try {
// 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}`;
// 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
console.log('Fetching RIASEC scores from:', 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
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

0
backend/user_profile Normal file
View File

Binary file not shown.

964
package-lock.json generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -10,18 +10,20 @@ export function CareerSuggestions({ careerSuggestions = [], onCareerClick }) {
<div>
<h2>Career Suggestions</h2>
<div className="career-suggestions-grid">
{careerSuggestions.map((career) => (
<button
key={career.code}
className="career-button"
onClick={() => onCareerClick(career)}
>
{career.title}
</button>
))}
{careerSuggestions.map((career) => {
return (
<button
key={career.code}
className="career-button"
onClick={() => onCareerClick(career)} // Directly pass the career to onCareerClick
>
{career.title}
</button>
);
})}
</div>
</div>
);
};
export default CareerSuggestions;
);
}
export default CareerSuggestions;

111
src/components/Chatbot.css Normal file
View 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
View 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! Im 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;

View File

@ -1,17 +1,24 @@
/* Dashboard.css */
/* Main Dashboard Layout */
.dashboard {
display: grid;
grid-template-columns: 1fr 2fr; /* Two columns: careers on the left, RIASEC on the right */
gap: 20px;
min-height: 100vh; /* Full height */
min-height: 100vh;
padding: 20px;
background-color: #f4f7fa;
}
/* Main Content Layout: Career Suggestions + RIASEC Scores */
.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 */
@ -24,6 +31,12 @@
border-left: 4px solid #6a9fb5;
}
.career-button.disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
.dashboard section:hover {
transform: translateY(-3px);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
@ -38,42 +51,104 @@ h2 {
padding-bottom: 4px;
}
/* Career Suggestions Grid */
/* Career Suggestions Section */
.career-suggestions-container {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1.5; /* Ensures it takes the majority of space */
width: 60%;
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 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* Flexible grid */
gap: 10px; /* Even spacing */
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
padding: 10px;
width: 100%;
justify-content: start;
}
/* Career Buttons */
.career-button {
padding: 10px;
padding: 8px 10px;
font-size: 14px;
font-weight: bold;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
border-radius: 6px;
cursor: pointer;
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 {
background-color: #0056b3;
}
/* RIASEC Scores and Descriptions */
/* RIASEC Section */
.riasec-container {
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
max-width: 400px; /* Ensure it stays visible */
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 {
padding: 15px;
border-radius: 8px;
@ -102,15 +177,19 @@ h2 {
color: #007bff;
}
/* Data Source Acknowledgment */
/* Acknowledgment Section - Move to Bottom */
.data-source-acknowledgment {
grid-column: span 2; /* Make acknowledgment span both columns */
margin-top: 20px;
padding: 10px;
border-top: 1px solid #ccc;
width: 100%;
text-align: center;
font-size: 12px;
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 */
@ -119,8 +198,18 @@ h2 {
max-width: 600px;
margin-left: 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-container {
text-align: center;
@ -140,3 +229,34 @@ h2 {
.sign-out-btn:hover {
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 */
}
}

View File

@ -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 { CareerSuggestions } from './CareerSuggestions.js';
import PopoutPanel from './PopoutPanel.js';
import './PopoutPanel.css';
import Chatbot from "./Chatbot.js";
import { Bar } from 'react-chartjs-2';
import { fetchSchools } from '../utils/apiUtils.js';
import './Dashboard.css';
@ -29,10 +31,57 @@ function Dashboard() {
const [areaTitle, setAreaTitle] = useState(null);
const [userZipcode, setUserZipcode] = useState(null);
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
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(() => {
let descriptions = []; // Declare outside for scope accessibility
@ -40,8 +89,8 @@ function Dashboard() {
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
descriptions = scores.map((score) => score.description || "No description available.");
setCareerSuggestions(suggestions || []);
setRiaSecScores(scores || []);
setRiaSecDescriptions(descriptions); // Set descriptions
setRiaSecScores(scores || []);
setRiaSecDescriptions(descriptions); // Set descriptions
} else {
console.warn('No data found, redirecting to Interest Inventory');
navigate('/interest-inventory');
@ -55,11 +104,11 @@ function Dashboard() {
const profileResponse = await fetch(`${apiUrl}/user-profile`, {
headers: { Authorization: `Bearer ${token}` },
});
if (profileResponse.ok) {
const profileData = await profileResponse.json();
console.log('Fetched User Profile:', profileData);
const { state, area, zipcode } = profileData; // Use 'area' instead of 'AREA_TITLE'
setUserState(state);
setAreaTitle(area && area.trim() ? area.trim() : ''); // Ensure 'area' is set correctly
@ -87,81 +136,45 @@ function Dashboard() {
setSalaryData([]); // Reset salary data
setEconomicProjections({}); // Reset economic projections
setTuitionData([]); // Reset tuition data
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
return;
}
try {
// Step 1: Fetch CIP Code
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
const { cipCode } = await cipResponse.json();
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
// Step 2: Fetch 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([
fetchSchools(cleanedCipCode, userState),
axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`),
axios.get(`${apiUrl}/tuition`, {
params: {
cipCode: cleanedCipCode,
state: userState
},
}),
axios.get(`${apiUrl}/salary`, {
params: {
socCode: socCode.split('.')[0],
area: areaTitle
},
}),
axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState }}),
axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle }}),
]);
// Check if `userZipcode` is set correctly
const currentUserZipcode = userZipcode;
if (!currentUserZipcode) {
console.error("User Zipcode is not set correctly:", currentUserZipcode);
return;
}
// Step 3: Add distance information to each school
// Handle Distance Calculation
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => {
// Combine Street Address, City, State, and ZIP to form the full school address
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
if (!currentUserZipcode || !schoolAddress) {
console.error('Missing ZIP codes or school address:', { currentUserZipcode, schoolAddress });
return { ...school, distance: 'Error', duration: 'Error' };
}
console.log("Calculating distance for User Zipcode:", currentUserZipcode, "and School Address:", schoolAddress);
try {
// Send the user's ZIP code and school address to the backend
const response = await axios.post(`${apiUrl}/maps/distance`, {
userZipcode: currentUserZipcode, // Pass the ZIP code (or lat/lng) of the user
destinations: schoolAddress, // Pass the full school address
});
const { distance, duration } = response.data;
return {
...school, // Keep all school data
distance, // Add the distance value
duration, // Add the duration value
};
} catch (error) {
console.error('Error fetching distance:', error);
return {
...school,
distance: 'Error',
duration: 'Error',
};
}
const response = await axios.post(`${apiUrl}/maps/distance`, {
userZipcode,
destinations: schoolAddress,
});
const { distance, duration } = response.data;
return { ...school, distance, duration };
}));
// Step 4: Format Salary Data
// Process Salary Data
const salaryDataPoints = [
{ percentile: '10th Percentile', value: salaryResponse.data.A_PCT10 || 0 },
{ percentile: '25th Percentile', value: salaryResponse.data.A_PCT25 || 0 },
@ -169,13 +182,15 @@ function Dashboard() {
{ percentile: '75th Percentile', value: salaryResponse.data.A_PCT75 || 0 },
{ percentile: '90th Percentile', value: salaryResponse.data.A_PCT90 || 0 },
];
// Step 5: Consolidate Career Details
// Consolidate Career Details with Job Description and Tasks
setCareerDetails({
...career,
jobDescription: description,
tasks: tasks,
economicProjections: economicResponse.data,
salaryData: salaryDataPoints,
schools: schoolsWithDistance, // Add schools with distances
schools: schoolsWithDistance,
tuitionData: tuitionResponse.data,
});
} catch (error) {
@ -185,9 +200,8 @@ function Dashboard() {
setLoading(false);
}
},
[userState, apiUrl, areaTitle]
[userState, apiUrl, areaTitle, userZipcode]
);
const chartData = {
labels: riaSecScores.map((score) => score.area),
@ -204,33 +218,48 @@ function Dashboard() {
return (
<div className="dashboard">
<div className="career-suggestions-container">
<CareerSuggestions careerSuggestions={careerSuggestions} onCareerClick={handleCareerClick} />
</div>
<div className="filter-container">
<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>
{/* Right RIASEC Chart + Descriptions */}
<div className="riasec-container">
<div className="riasec-scores">
<h2>RIASEC Scores</h2>
<Bar data={chartData} />
<div className="dashboard-content">
<div className="career-suggestions-container">
<CareerSuggestions careerSuggestions={filteredCareers} onCareerClick={handleCareerClick} />
</div>
<div className="riasec-descriptions">
<h3>RIASEC Personality Descriptions</h3>
{riaSecDescriptions.length > 0 ? (
<ul>
{riaSecDescriptions.map((desc, index) => (
<li key={index}>
<strong>{riaSecScores[index]?.area}:</strong> {desc}
</li>
))}
</ul>
) : (
<p>Loading descriptions...</p>
)}
<div className="riasec-container">
<div className="riasec-scores">
<h2>RIASEC Scores</h2>
<Bar data={chartData} />
</div>
<div className="riasec-descriptions">
<h3>RIASEC Personality Descriptions</h3>
{riaSecDescriptions.length > 0 ? (
<ul>
{riaSecDescriptions.map((desc, index) => (
<li key={index}>
<strong>{riaSecScores[index]?.area}:</strong> {desc}
</li>
))}
</ul>
) : (
<p>Loading descriptions...</p>
)}
</div>
</div>
</div>
{selectedCareer && (
<PopoutPanel
data={careerDetails}
@ -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 */}
<div
className="data-source-acknowledgment"
@ -257,6 +305,24 @@ function Dashboard() {
textAlign: 'center'
}}
>
<div className="chatbot-widget">
<Chatbot
context={{
careerSuggestions,
riaSecScores,
selectedCareer,
schools,
salaryData,
economicProjections,
tuitionData,
userState,
areaTitle,
userZipcode,
}}
/>
</div>
<p>
Career results and RIASEC scores are provided by
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>, in conjunction with the
@ -268,4 +334,4 @@ function Dashboard() {
);
}
export default Dashboard;
export default Dashboard;

View 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;
}

View File

@ -1,60 +1,57 @@
import React, { useState } from 'react';
import './LoanRepayment.css';
function LoanRepayment({ schools, salaryData, earningHorizon }) {
const [tuitionType, setTuitionType] = useState('inState'); // 'inState' or 'outOfState'
const [interestRate, setInterestRate] = useState(5.5); // Default federal loan interest rate
const [loanTerm, setLoanTerm] = useState(10); // Default loan term (10 years)
function LoanRepayment({
schools,
salaryData,
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 [currentSalary, setCurrentSalary] = useState(0); // Current salary input
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Validation function
const validateInputs = () => {
if (!schools || schools.length === 0) {
setError('School data is missing. Loan calculations cannot proceed.');
return false;
}
if (interestRate <= 0) {
setError('Interest rate must be greater than 0.');
return false;
}
if (loanTerm <= 0) {
setError('Loan term must be greater than 0.');
return false;
}
if (extraPayment < 0) {
setError('Extra monthly payment cannot be negative.');
return false;
}
if (currentSalary < 0) {
setError('Current salary cannot be negative.');
return false;
}
setError(null); // Clear errors if valid
setError(null);
return true;
};
// Loan calculation function for all schools
const calculateLoanDetails = () => {
if (!validateInputs()) return; // Validate inputs before calculation
if (!validateInputs()) return;
setLoading(true);
const schoolResults = schools.map((school) => {
const tuition = tuitionType === 'inState' ? school.inState : school.outOfState;
const monthlyRate = interestRate / 12 / 100;
const loanTermMonths = loanTerm * 12;
// Calculate minimum monthly payment
const minimumMonthlyPayment = tuition * (monthlyRate * Math.pow(1 + monthlyRate, loanTermMonths)) /
(Math.pow(1 + monthlyRate, loanTermMonths) - 1);
// Total loan cost with extra payments
const extraMonthlyPayment = minimumMonthlyPayment + extraPayment;
let remainingBalance = tuition;
let monthsWithExtra = 0;
@ -68,18 +65,14 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
const totalLoanCost = extraMonthlyPayment * monthsWithExtra;
// Handle missing salary data
let salary = salaryData && salaryData[0]?.value ? salaryData[0].value : null;
let salary = salaryData.find((point) => point.percentile === selectedSalary)?.value || 0;
let netGain = 'N/A';
let monthlySalary = 'N/A';
if (salary) {
// Calculate net gain
const totalSalary = salary * earningHorizon;
const currentSalaryEarnings = currentSalary * earningHorizon * Math.pow(1.03, earningHorizon); // 3% growth
if (salary > 0) {
const totalSalary = salary * loanTerm;
const currentSalaryEarnings = currentSalary * loanTerm * Math.pow(1.03, loanTerm);
netGain = (totalSalary - totalLoanCost - currentSalaryEarnings).toFixed(2);
// Monthly salary
monthlySalary = (salary / 12).toFixed(2);
}
@ -87,7 +80,7 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
...school,
tuition,
monthlyPayment: minimumMonthlyPayment.toFixed(2),
totalMonthlyPayment: extraMonthlyPayment.toFixed(2), // Add total payment including extra
totalMonthlyPayment: extraMonthlyPayment.toFixed(2),
totalLoanCost: totalLoanCost.toFixed(2),
netGain,
monthlySalary,
@ -95,102 +88,48 @@ function LoanRepayment({ schools, salaryData, earningHorizon }) {
});
setResults(schoolResults);
setLoading(false);
};
return (
<div>
<h2>Loan Repayment and ROI Analysis</h2>
<div>
<label>
Tuition Type:
<select
value={tuitionType}
onChange={(e) => setTuitionType(e.target.value)}
>
<div className="loan-repayment-container">
<form onSubmit={(e) => { e.preventDefault(); calculateLoanDetails(); }}>
<div>
<label>Tuition Type:</label>
<select value={tuitionType} onChange={(e) => setTuitionType(e.target.value)}>
<option value="inState">In-State</option>
<option value="outOfState">Out-of-State</option>
</select>
</label>
<label>
Interest Rate (%):
<input
type="number"
value={interestRate}
onChange={(e) => setInterestRate(Number(e.target.value))}
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>
{/* Error Message */}
{error && <p style={{ color: 'red' }}>{error}</p>}
{/* Results Display */}
{results.length > 0 && (
<div>
<h3>Comparison by School</h3>
{results.map((result, index) => (
<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>
)}
{/* Salary Warning */}
{!salaryData || salaryData.length === 0 ? (
<p style={{ color: 'red' }}>Salary data is not available for this profession. Loan calculations are limited.</p>
) : null}
<div>
<label>Interest Rate:</label>
<input type="number" value={interestRate} onChange={(e) => setInterestRate(e.target.value)} />
</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>
<button type="submit">Calculate</button>
</form>
{error && <div className="error">{error}</div>}
</div>
);
}
export default LoanRepayment;
export default LoanRepayment;

View File

@ -2,7 +2,7 @@
position: fixed;
top: 0;
right: 0;
width: 40%; /* Default width for larger screens */
width: 60%; /* Increase width for larger screens */
height: 100%;
background-color: #fff;
box-shadow: -3px 0 5px rgba(0, 0, 0, 0.3);
@ -16,14 +16,18 @@
/* Mobile responsiveness */
@media (max-width: 768px) {
.popout-panel {
width: 100%; /* Use full width for smaller screens */
height: 100%; /* Cover full height */
left: 0; /* Ensure it appears on the left for mobile */
right: unset; /* Override right alignment */
width: 100%; /* Use full width for smaller screens */
height: 100%; /* Cover full height */
left: 0; /* Ensure it appears on the left for mobile */
right: unset; /* Override right alignment */
}
.schools-offering {
grid-template-columns: 1fr; /* Single column layout for smaller screens */
}
}
/* Close button adjustments for mobile */
/* Close button adjustments for mobile */
.close-btn {
position: absolute;
top: 10px;
@ -37,39 +41,279 @@
z-index: 1001; /* Keep button above the panel */
}
h3 {
margin-top: 20px;
}
ul {
list-style: none;
padding: 0;
}
li {
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
}
button {
margin-top: 10px;
background-color: #007bff;
color: white;
border: none;
padding: 8px 12px;
cursor: pointer;
font-size: 0.9rem;
}
button:hover {
background-color: #0056b3;
}
/* Fix for overflow issue */
html, body {
overflow-x: hidden; /* Prevent horizontal scrolling */
/* Job Description and Expected Tasks section */
.section {
margin-bottom: 20px;
}
.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;
}
.expected-tasks ul {
list-style-position: inside; /* Move the bullets inside, aligning them with text */
padding-left: 20px; /* Add space between the bullet and the text */
margin: 0;
text-align: left; /* Align the text to the left */
}
.expected-tasks li {
margin-bottom: 15px; /* Space between each task */
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
font-size: 1rem;
}
/* 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;
background-color: #4CAF50;
color: white;
border: none;
text-align: center;
font-size: 1.1rem;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 8px;
transition: background-color 0.3s;
}
.loan-repayment-fields button:hover {
background-color: #45a049;
}
.loan-repayment-fields button:disabled {
background-color: #ccc;
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 */
}

View File

@ -1,17 +1,22 @@
import { ClipLoader } from 'react-spinners';
import LoanRepayment from './LoanRepayment.js';
import './PopoutPanel.css';
import { useState } from 'react';
function PopoutPanel({
function PopoutPanel({
data = {},
userState = 'N/A', // Passed explicitly from Dashboard
loading = false,
error = null,
closePanel
loading = false,
error = null,
closePanel,
}) {
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) {
return (
<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
const {
title = 'Career Details',
economicProjections = {},
salaryData = [],
schools = [],
tuitionData = []
const {
jobDescription = null, // Default to null if not provided
tasks = null, // Default to null if not provided
title = 'Career Details',
economicProjections = {},
salaryData = [],
schools = [],
} = data;
const tenthPercentileSalary = salaryData?.find(
(point) => point.percentile === '10th Percentile'
)?.value || 0;
// Get program length for calculating tuition
const getProgramLength = (degreeType) => {
if (degreeType?.includes("Associate")) return 2;
if (degreeType?.includes("Bachelor")) return 4;
@ -69,104 +52,123 @@ function PopoutPanel({
<button onClick={closePanel}>Close</button>
<h2>{title}</h2>
{/* Schools Offering Programs */}
<h3>Schools Offering Programs</h3>
{Array.isArray(schools) && schools.length > 0 ? (
<ul>
{schools.map((school, index) => {
const matchingTuitionData = tuitionData.find(
(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>
) : (
<p>No schools of higher education are available for this career path.</p>
)}
{/* Job Description and Tasks */}
<div className="job-description">
<h3>Job Description</h3>
<p>{jobDescription || 'No description available'}</p>
</div>
{/* Economic Projections */}
<h3>Economic Projections for {userState}</h3>
{economicProjections && typeof economicProjections === 'object' ? (
<ul>
<li>2022 Employment: {economicProjections['2022 Employment'] || 'N/A'}</li>
<li>2032 Employment: {economicProjections['2032 Employment'] || 'N/A'}</li>
<li>Total Change: {economicProjections['Total Change'] || 'N/A'}</li>
</ul>
) : (
<p>No economic projections available for this career path.</p>
)}
{/* Salary Data Points */}
<h3>Salary Data</h3>
{salaryData && salaryData.length > 0 ? (
<table>
<thead>
<tr>
<th>Percentile</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
{salaryData.map((point, index) => (
<tr key={index}>
<td>{point.percentile}</td>
<td>
{point.value > 0 ? `$${parseInt(point.value, 10).toLocaleString()}` : 'N/A'}
</td>
</tr>
<div className="job-tasks">
<h3>Expected Tasks</h3>
{tasks && tasks.length > 0 ? (
<ul>
{tasks.map((task, index) => (
<li key={index}>{task}</li>
))}
</tbody>
</table>
) : (
<p>No salary data is available for this career path.</p>
)}
{/* Loan Repayment Analysis */}
<div className="loan-repayment-analysis">
<h3>Loan Repayment Analysis</h3>
<LoanRepayment
schools={schools.map((school) => {
const years = getProgramLength(school['CREDDESC']);
return {
schoolName: school['INSTNM'],
inState: parseFloat(school['In_state cost'] * years) || 0,
outOfState: parseFloat(school['Out_state cost'] * years) || 0,
};
})}
salaryData={
tenthPercentileSalary > 0
? [{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]
: []
}
earningHorizon={10}
/>
{!tenthPercentileSalary && (
<p>
<em>Salary data unavailable. Loan details are based on cost alone.</em>
</p>
</ul>
) : (
<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 */}
<div className="economic-projections">
<h3>Economic Projections for {userState}</h3>
{economicProjections && typeof economicProjections === 'object' ? (
<ul>
<li>2022 Employment: {economicProjections['2022 Employment'] || 'N/A'}</li>
<li>2032 Employment: {economicProjections['2032 Employment'] || 'N/A'}</li>
<li>Total Change: {economicProjections['Total Change'] || 'N/A'}</li>
</ul>
) : (
<p>No economic projections available for this career path.</p>
)}
</div>
{/* Salary Data Points */}
<div className="salary-data">
<h3>Salary Data</h3>
{salaryData.length > 0 ? (
<table>
<thead>
<tr>
<th>Percentile</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
{salaryData.map((point, index) => (
<tr key={index}>
<td>{point.percentile}</td>
<td>{point.value > 0 ? `$${parseInt(point.value, 10).toLocaleString()}` : 'N/A'}</td>
</tr>
))}
</tbody>
</table>
) : (
<p>Salary data is not available.</p>
)}
</div>
{/* Loan Repayment Analysis */}
<h3>Loan Repayment Analysis</h3>
{/* Loan Repayment Calculation Results */}
<LoanRepayment
schools={schools.map((school) => {
const years = getProgramLength(school['CREDDESC']);
return {
schoolName: school['INSTNM'],
inState: parseFloat(school['In_state cost'] * years) || 0,
outOfState: parseFloat(school['Out_state cost'] * years) || 0,
degreeType: school['CREDDESC'],
};
})}
salaryData={salaryData}
setResults={setResults}
setLoading={setLoadingCalculation}
/>
{/* Results Display */}
{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>Monthly Salary (Gross): {result.monthlySalary}</p>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -32,6 +32,9 @@
max-width: 280px; /* Optional max width */
padding: 10px;
background-color: #4CAF50;
justify-content: center;
align-items: center;
text-align: center;
color: #fff;
border: none;
border-radius: 5px;

View File

@ -24,7 +24,7 @@ function SignIn({ setIsAuthenticated }) {
}
try {
// 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',
headers: {
'Content-Type': 'application/json',
@ -45,7 +45,7 @@ function SignIn({ setIsAuthenticated }) {
localStorage.setItem('userId', userId);
console.log('Token and userId saved in localStorage');
console.log('SignIn response data:', data);
// Call setIsAuthenticated(true) to update the state
setIsAuthenticated(true);
navigate('/getting-started'); // Redirect to GettingStarted after SignIn

View File

@ -3,7 +3,6 @@ import axios from 'axios';
//fetch areas by state
export const fetchAreasByState = async (state) => {
try {
const apiUrl = process.env.REACT_APP_API_URL || '';
const response = await fetch(`${process.env.REACT_APP_API_URL}/Institution_data.json`);
if (response.status === 200) {

102
src/utils/fetchJobZones.js Normal file
View 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
View File

Binary file not shown.