Fixed Economic Projections' data source to include and display National
This commit is contained in:
parent
c44f2e54f2
commit
7651b5e5e7
@ -3,56 +3,62 @@ import axios from 'axios';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet'; // Import helmet for HTTP security headers
|
||||
import dotenv from 'dotenv';
|
||||
import xlsx from 'xlsx'; // Import xlsx to read the Excel file
|
||||
import xlsx from 'xlsx'; // Keep for CIP->SOC mapping only
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url'; // Import fileURLToPath to handle the current file's URL
|
||||
import { open } from 'sqlite'; // Use the open method directly from sqlite package
|
||||
import { fileURLToPath } from 'url';
|
||||
import { open } from 'sqlite';
|
||||
import sqlite3 from 'sqlite3';
|
||||
import fs from 'fs';
|
||||
import readline from 'readline';
|
||||
|
||||
// Correct the order of variable initialization
|
||||
// --- Basic file init ---
|
||||
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
|
||||
const rootPath = path.resolve(__dirname, '..'); // Up one level
|
||||
const env = process.env.NODE_ENV?.trim() || 'development';
|
||||
const envPath = path.resolve(rootPath, `.env.${env}`);
|
||||
dotenv.config({ path: envPath }); // Load .env
|
||||
|
||||
// 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
|
||||
// Whitelist CORS
|
||||
const allowedOrigins = [
|
||||
'http://localhost:3000',
|
||||
'http://34.16.120.118:3000',
|
||||
'https://dev1.aptivaai.com'
|
||||
];
|
||||
|
||||
|
||||
const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'https://dev1.aptivaai.com'];
|
||||
// CIP->SOC mapping file
|
||||
const mappingFilePath = '/home/jcoakley/aptiva-dev1-app/public/CIP_to_ONET_SOC.xlsx';
|
||||
|
||||
// Institution data
|
||||
const institutionFilePath = path.resolve(rootPath, 'public/Institution_data.json');
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5001;
|
||||
|
||||
// Initialize database connection
|
||||
/**************************************************
|
||||
* DB connections
|
||||
**************************************************/
|
||||
let db;
|
||||
const initDB = async () => {
|
||||
try {
|
||||
// Opening SQLite connection using sqlite's open function and sqlite3 as the driver
|
||||
db = await open({
|
||||
filename: '/home/jcoakley/aptiva-dev1-app/salary_info.db', // Path to SQLite DB file
|
||||
driver: sqlite3.Database, // Use sqlite3's Database driver
|
||||
filename: '/home/jcoakley/aptiva-dev1-app/salary_info.db',
|
||||
driver: sqlite3.Database,
|
||||
});
|
||||
console.log('Connected to SQLite database.');
|
||||
} catch (error) {
|
||||
console.error('Error connecting to database:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize database before starting the server
|
||||
initDB();
|
||||
|
||||
let userProfileDb;
|
||||
const initUserProfileDb = async () => {
|
||||
try {
|
||||
userProfileDb = await open({
|
||||
filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db', // Path to user_profile.db
|
||||
filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db',
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
console.log('Connected to user_profile.db.');
|
||||
@ -60,24 +66,21 @@ const initUserProfileDb = async () => {
|
||||
console.error('Error connecting to user_profile.db:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize user_profile.db before starting the server
|
||||
initUserProfileDb();
|
||||
|
||||
// Add security headers using helmet
|
||||
/**************************************************
|
||||
* Security, CORS, JSON Body
|
||||
**************************************************/
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: false, // Disable CSP for now; enable as needed later
|
||||
contentSecurityPolicy: false,
|
||||
crossOriginEmbedderPolicy: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Updated CORS Middleware for dynamic handling of origins and static files
|
||||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
|
||||
if (origin && allowedOrigins.includes(origin)) {
|
||||
// Allow requests from whitelisted frontend origins
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
res.setHeader(
|
||||
@ -86,15 +89,15 @@ app.use((req, res, next) => {
|
||||
);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
} else if (req.path.includes('Institution_data')) {
|
||||
// Handle static JSON file CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*'); // Allow all origins for static file
|
||||
// For that JSON
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.setHeader(
|
||||
'Access-Control-Allow-Headers',
|
||||
'Content-Type, Accept, Origin, X-Requested-With'
|
||||
);
|
||||
} else {
|
||||
// Default headers for other unhandled cases
|
||||
// default
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader(
|
||||
@ -102,43 +105,80 @@ app.use((req, res, next) => {
|
||||
'Authorization, Content-Type, Accept, Origin, X-Requested-With'
|
||||
);
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
|
||||
return res.status(204).end(); // Use 204 for no content
|
||||
return res.status(204).end();
|
||||
}
|
||||
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware to parse JSON bodies
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// For completeness
|
||||
app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
/**************************************************
|
||||
* Load CIP->SOC mapping
|
||||
**************************************************/
|
||||
function loadMapping() {
|
||||
try {
|
||||
const workbook = xlsx.readFile(mappingFilePath);
|
||||
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
return xlsx.utils.sheet_to_json(sheet);
|
||||
} catch (error) {
|
||||
console.error('Error reading CIP_to_ONET_SOC:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const socToCipMapping = loadMapping();
|
||||
if (socToCipMapping.length === 0) {
|
||||
console.error("SOC to CIP mapping data is empty.");
|
||||
}
|
||||
|
||||
// Route to fetch O*Net Interest Inventory questions with pagination
|
||||
/**************************************************
|
||||
* Load single JSON with all states + US
|
||||
* Replaces old GA-only approach
|
||||
**************************************************/
|
||||
// const projectionsFilePath = path.resolve(__dirname, '..', 'public', 'occprj.xlsx');
|
||||
// const workbook = xlsx.readFile(projectionsFilePath);
|
||||
// const sheet = workbook.Sheets['GAOccProj 2022-2032'];
|
||||
// const projectionsData = xlsx.utils.sheet_to_json(sheet, { header: 1 });
|
||||
|
||||
const singleProjFile = path.resolve(__dirname, '..', 'public', 'economicproj.json');
|
||||
let allProjections = [];
|
||||
try {
|
||||
const raw = fs.readFileSync(singleProjFile, 'utf8');
|
||||
allProjections = JSON.parse(raw);
|
||||
console.log(`Loaded ${allProjections.length} rows from economicproj.json`);
|
||||
} catch (err) {
|
||||
console.error('Error reading economicproj.json:', err);
|
||||
}
|
||||
|
||||
/**************************************************
|
||||
* ONet routes, CIP routes, distance routes, etc.
|
||||
**************************************************/
|
||||
|
||||
// O*Net interest questions
|
||||
app.get('/api/onet/questions', async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
|
||||
if (!start || !end) {
|
||||
return res.status(400).json({ error: 'Start and end parameters are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const questions = [];
|
||||
let currentStart = parseInt(start, 10);
|
||||
let currentEnd = parseInt(end, 10);
|
||||
|
||||
while (currentStart <= currentEnd) {
|
||||
// Fetch questions from O*Net API for the current range
|
||||
const response = await axios.get(
|
||||
`https://services.onetcenter.org/ws/mnm/interestprofiler/questions?start=${currentStart}&end=${Math.min(currentEnd, currentStart + 11)}`,
|
||||
`https://services.onetcenter.org/ws/mnm/interestprofiler/questions?start=${currentStart}&end=${Math.min(
|
||||
currentEnd,
|
||||
currentStart + 11
|
||||
)}`,
|
||||
{
|
||||
auth: {
|
||||
username: process.env.ONET_USERNAME,
|
||||
@ -146,24 +186,18 @@ app.get('/api/onet/questions', async (req, res) => {
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Add questions to the result set
|
||||
if (response.data.question && Array.isArray(response.data.question)) {
|
||||
questions.push(...response.data.question);
|
||||
}
|
||||
|
||||
// Check if there's a next page
|
||||
const nextLink = response.data.link?.find((link) => link.rel === 'next');
|
||||
if (nextLink) {
|
||||
// Update start and end based on the "next" link
|
||||
const nextParams = new URLSearchParams(nextLink.href.split('?')[1]);
|
||||
currentStart = parseInt(nextParams.get('start'), 10);
|
||||
currentEnd = parseInt(nextParams.get('end'), 10);
|
||||
} else {
|
||||
break; // Stop if there are no more pages
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ questions });
|
||||
} catch (error) {
|
||||
console.error('Error fetching O*Net questions:', error.message);
|
||||
@ -171,22 +205,19 @@ app.get('/api/onet/questions', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to geocode an address or zip code
|
||||
const geocodeZipCode = async (zipCode) => {
|
||||
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||
// geocode
|
||||
async function geocodeZipCode(zipCode) {
|
||||
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error('Google Maps API Key is missing.');
|
||||
} else {
|
||||
|
||||
console.error('Google Maps API Key missing');
|
||||
}
|
||||
try {
|
||||
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(zipCode)}&components=country:US&key=${apiKey}`;
|
||||
|
||||
const geocodeUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
|
||||
zipCode
|
||||
)}&components=country:US&key=${apiKey}`;
|
||||
const response = await axios.get(geocodeUrl);
|
||||
|
||||
if (response.data.status === 'OK' && response.data.results.length > 0) {
|
||||
const location = response.data.results[0].geometry.location;
|
||||
return location; // Return the latitude and longitude
|
||||
return response.data.results[0].geometry.location;
|
||||
} else {
|
||||
throw new Error('Geocoding failed');
|
||||
}
|
||||
@ -194,492 +225,394 @@ const geocodeZipCode = async (zipCode) => {
|
||||
console.error('Error geocoding ZIP code:', error.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Distance
|
||||
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.' });
|
||||
return res.status(400).json({ error: 'User ZIP code and destination are required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY; // Get the API key
|
||||
|
||||
// Geocode the user's ZIP code
|
||||
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||
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
|
||||
|
||||
// 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 origins = `${userLocation.lat},${userLocation.lng}`;
|
||||
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 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during distance calculation:', error);
|
||||
res.status(500).json({ error: 'Internal server error', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Load the economic projections data from the Excel file
|
||||
const projectionsFilePath = path.resolve(__dirname, '..', 'public', 'occprj.xlsx'); // Adjusted path
|
||||
const workbook = xlsx.readFile(projectionsFilePath);
|
||||
const sheet = workbook.Sheets['GAOccProj 2022-2032']; // The sheet with your data
|
||||
const projectionsData = xlsx.utils.sheet_to_json(sheet, { header: 1 }); // Convert the sheet to JSON
|
||||
|
||||
const loadMapping = () => {
|
||||
try {
|
||||
const workbook = xlsx.readFile(mappingFilePath); // Read the Excel file
|
||||
const sheet = workbook.Sheets[workbook.SheetNames[0]]; // Assuming the first sheet
|
||||
const data = xlsx.utils.sheet_to_json(sheet); // Convert the sheet to JSON
|
||||
return data; // Return the data from the Excel sheet
|
||||
} catch (error) {
|
||||
console.error('Error reading the CIP to ONET SOC mapping file:', error);
|
||||
return []; // Return an empty array if there's an issue loading the mapping
|
||||
}
|
||||
};
|
||||
|
||||
// Load the mapping data from the Excel file
|
||||
const socToCipMapping = loadMapping();
|
||||
if (socToCipMapping.length === 0) {
|
||||
console.error("SOC to CIP mapping data is empty.");
|
||||
}
|
||||
|
||||
const filterHigherEducationCareers = (careers) => {
|
||||
return careers
|
||||
.map((career) => {
|
||||
const educationLevel = career.education;
|
||||
if (
|
||||
![
|
||||
"No formal education",
|
||||
"High school",
|
||||
"Some college, no degree",
|
||||
].includes(educationLevel)
|
||||
) {
|
||||
return {
|
||||
href: career.href,
|
||||
fit: career.fit,
|
||||
code: career.code,
|
||||
title: career.title,
|
||||
tags: career.tags,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((career) => career); // Remove undefined values
|
||||
};
|
||||
|
||||
// Route to handle submission of answers to O*Net API for career suggestions and RIASEC scores
|
||||
// ONet submission
|
||||
app.post('/api/onet/submit_answers', async (req, res) => {
|
||||
console.log('POST /api/onet/submit_answers hit');
|
||||
const { answers } = req.body; // Get answers string from the request body
|
||||
|
||||
const { answers } = req.body;
|
||||
if (!answers || answers.length !== 60) {
|
||||
console.error('Invalid answers provided:', answers);
|
||||
return res.status(400).json({ error: 'Answers parameter must be a 60-character string.' });
|
||||
console.error('Invalid answers:', answers);
|
||||
return res.status(400).json({ error: 'Answers must be 60 chars long.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// URLs for career suggestions and RIASEC scores
|
||||
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
|
||||
console.log('Fetching career suggestions from:', careerUrl);
|
||||
// career suggestions
|
||||
const careerResponse = await axios.get(careerUrl, {
|
||||
auth: {
|
||||
username: process.env.ONET_USERNAME,
|
||||
password: process.env.ONET_PASSWORD
|
||||
password: process.env.ONET_PASSWORD,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
|
||||
console.log('Career API Response:', JSON.stringify(careerResponse.data, null, 2));
|
||||
|
||||
// Fetch RIASEC scores
|
||||
console.log('Fetching RIASEC scores from:', resultsUrl);
|
||||
// RIASEC
|
||||
const resultsResponse = await axios.get(resultsUrl, {
|
||||
auth: {
|
||||
username: process.env.ONET_USERNAME,
|
||||
password: process.env.ONET_PASSWORD
|
||||
password: process.env.ONET_PASSWORD,
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
|
||||
// Extract career suggestions and RIASEC scores
|
||||
const careerSuggestions = careerResponse.data.career || []; // Initialize the array
|
||||
const careerSuggestions = careerResponse.data.career || [];
|
||||
const riaSecScores = resultsResponse.data.result || [];
|
||||
|
||||
// Apply filtering based on education requirements INSIDE the try block
|
||||
const filteredCareers = filterHigherEducationCareers(careerSuggestions);
|
||||
// filter out lower ed
|
||||
const filtered = filterHigherEducationCareers(careerSuggestions);
|
||||
|
||||
// Logging for debugging
|
||||
console.log('Raw Career Suggestions:', careerSuggestions);
|
||||
console.log('RIASEC Scores:', riaSecScores);
|
||||
console.log('Filtered Careers:', filteredCareers);
|
||||
|
||||
// Send the combined data to the frontend
|
||||
res.status(200).json({
|
||||
careers: filteredCareers,
|
||||
riaSecScores: riaSecScores
|
||||
careers: filtered,
|
||||
riaSecScores,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data from O*Net API:', error.response?.data || error.message);
|
||||
console.error('Error in ONet API:', error.response?.data || error.message);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch data from O*Net API',
|
||||
error: 'Failed to fetch data from ONet',
|
||||
details: error.response?.data || error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
function filterHigherEducationCareers(careers) {
|
||||
return careers
|
||||
.map((c) => {
|
||||
const edu = c.education;
|
||||
if (!['No formal education', 'High school', 'Some college, no degree'].includes(edu)) {
|
||||
return {
|
||||
href: c.href,
|
||||
fit: c.fit,
|
||||
code: c.code,
|
||||
title: c.title,
|
||||
tags: c.tags
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
||||
// ONet career details
|
||||
app.get('/api/onet/career-details/:socCode', async (req, res) => {
|
||||
const { socCode } = req.params;
|
||||
|
||||
if (!socCode) {
|
||||
return res.status(400).json({ error: 'SOC Code is required.' });
|
||||
return res.status(400).json({ error: 'SOC code is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
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',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
res.status(200).json(response.data); // Forward the API response to the frontend
|
||||
} catch (error) {
|
||||
console.error('Error fetching career details:', error.message);
|
||||
res.status(500).json({ error: 'Failed to fetch career details from O*NET API.' });
|
||||
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' },
|
||||
});
|
||||
res.status(200).json(response.data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching career details:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch career details' });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to fetch career description and tasks from O*Net
|
||||
// ONet career description
|
||||
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.' });
|
||||
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
|
||||
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' },
|
||||
});
|
||||
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 { what_they_do, on_the_job } = response.data;
|
||||
const tasks = on_the_job?.task || [];
|
||||
|
||||
// Prepare the data for the frontend
|
||||
const careerOverview = {
|
||||
return res.json({
|
||||
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.' });
|
||||
tasks: tasks.length ? tasks : ['No tasks available']
|
||||
});
|
||||
}
|
||||
return res.status(404).json({ error: 'Career not found for 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.' });
|
||||
console.error('Error in career-description route:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch career description' });
|
||||
}
|
||||
});
|
||||
|
||||
// Route to handle fetching CIP code based on SOC code
|
||||
|
||||
// CIP route
|
||||
app.get('/api/cip/:socCode', (req, res) => {
|
||||
const { socCode } = req.params;
|
||||
console.log(`Received SOC Code: ${socCode.trim()}`);
|
||||
|
||||
for (let row of socToCipMapping) {
|
||||
const mappedSOC = row['O*NET-SOC 2019 Code']?.trim(); // Trim spaces
|
||||
const mappedSOC = row['O*NET-SOC 2019 Code']?.trim();
|
||||
if (mappedSOC === socCode.trim()) {
|
||||
console.log('Found matching CIP Code:', row['2020 CIP Code']);
|
||||
console.log('Found CIP code:', row['2020 CIP Code']);
|
||||
return res.json({ cipCode: row['2020 CIP Code'] });
|
||||
}
|
||||
}
|
||||
|
||||
console.error('SOC code not found in mapping:', socCode);
|
||||
res.status(404).json({ error: 'CIP code not found for this SOC code' });
|
||||
res.status(404).json({ error: 'CIP code not found' });
|
||||
});
|
||||
|
||||
// Filtered schools endpoint
|
||||
/**************************************************
|
||||
* Single schools / tuition / etc. routes
|
||||
**************************************************/
|
||||
app.get('/api/schools', (req, res) => {
|
||||
const { cipCode, state } = req.query;
|
||||
console.log('Query Params:', { cipCode, state });
|
||||
|
||||
// Validate required parameters
|
||||
if (!cipCode || !state) {
|
||||
return res.status(400).json({ error: 'CIP Code and State are required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Normalize CIP Code
|
||||
const matchedCIP = cipCode.replace('.', '').slice(0, 4);
|
||||
|
||||
let schoolsData = [];
|
||||
try {
|
||||
const rawData = fs.readFileSync(institutionFilePath, 'utf8');
|
||||
schoolsData = JSON.parse(rawData); // Parse entire JSON array
|
||||
} catch (error) {
|
||||
console.error('Error parsing institution data:', error.message);
|
||||
return res.status(500).json({ error: 'Failed to load schools data.' });
|
||||
}
|
||||
|
||||
|
||||
// Filter based on CIP code and state
|
||||
const filteredSchools = schoolsData.filter((school) => {
|
||||
const schoolCIP = school['CIPCODE']?.toString().replace('.', '').slice(0, 4);
|
||||
const schoolState = school['State']?.toUpperCase().trim();
|
||||
|
||||
return (
|
||||
schoolCIP.startsWith(matchedCIP) &&
|
||||
schoolState === state.toUpperCase().trim()
|
||||
);
|
||||
try {
|
||||
const rawData = fs.readFileSync(institutionFilePath, 'utf8');
|
||||
schoolsData = JSON.parse(rawData);
|
||||
} catch (err) {
|
||||
console.error('Error parsing institution data:', err.message);
|
||||
return res.status(500).json({ error: 'Failed to load schools data.' });
|
||||
}
|
||||
const filtered = schoolsData.filter((s) => {
|
||||
const scip = s['CIPCODE']?.toString().replace('.', '').slice(0, 4);
|
||||
const st = s['State']?.toUpperCase().trim();
|
||||
return scip.startsWith(matchedCIP) && st === state.toUpperCase().trim();
|
||||
});
|
||||
|
||||
console.log('Filtered Schools Count:', filteredSchools.length);
|
||||
res.json(filteredSchools); // Return filtered results
|
||||
} catch (error) {
|
||||
console.error('Error reading Institution data:', error.message);
|
||||
console.log('Filtered schools:', filtered.length);
|
||||
res.json(filtered);
|
||||
} catch (err) {
|
||||
console.error('Error reading Institution data:', err.message);
|
||||
res.status(500).json({ error: 'Failed to load schools data.' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Route to handle fetching tuition data using CIP code and state as query parameters
|
||||
// tuition
|
||||
app.get('/api/tuition', (req, res) => {
|
||||
const { cipCode, state } = req.query; // Extract query parameters
|
||||
console.log(`Received CIP Code: ${cipCode}, State: ${state}`);
|
||||
|
||||
// Validate required parameters
|
||||
const { cipCode, state } = req.query;
|
||||
console.log(`Received CIP: ${cipCode}, State: ${state}`);
|
||||
if (!cipCode || !state) {
|
||||
return res.status(400).json({ error: 'CIP Code and State are required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const schoolsData = JSON.parse(fs.readFileSync(institutionFilePath, 'utf8'));
|
||||
|
||||
console.log('Loaded Tuition Data:', schoolsData.length);
|
||||
|
||||
// Filter data by CIP Code and State
|
||||
const filteredData = schoolsData.filter((school) => {
|
||||
const cipCodeValue = school['CIPCODE']?.toString().replace(/[^0-9]/g, ''); // Normalize CIP Code
|
||||
const stateValue = school['State']?.toUpperCase().trim(); // Normalize State
|
||||
|
||||
return (
|
||||
cipCodeValue.startsWith(cipCode) && // Partial match CIP Code
|
||||
stateValue === state.toUpperCase().trim() // Exact match State
|
||||
);
|
||||
const raw = fs.readFileSync(institutionFilePath, 'utf8');
|
||||
const schoolsData = JSON.parse(raw);
|
||||
const filtered = schoolsData.filter((school) => {
|
||||
const cval = school['CIPCODE']?.toString().replace(/[^0-9]/g, '');
|
||||
const sVal = school['State']?.toUpperCase().trim();
|
||||
return cval.startsWith(cipCode) && sVal === state.toUpperCase().trim();
|
||||
});
|
||||
|
||||
console.log('Filtered Tuition Data Count:', filteredData.length);
|
||||
res.json(filteredData); // Return filtered results
|
||||
} catch (error) {
|
||||
console.error('Error reading tuition data:', error.message);
|
||||
console.log('Filtered Tuition Data Count:', filtered.length);
|
||||
res.json(filtered);
|
||||
} catch (err) {
|
||||
console.error('Error reading tuition data:', err.message);
|
||||
res.status(500).json({ error: 'Failed to load tuition data.' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Route to handle fetching economic projections for SOC code
|
||||
/**************************************************
|
||||
* SINGLE route for projections from economicproj.json
|
||||
**************************************************/
|
||||
// Remove old GA Excel approach; unify with single JSON approach
|
||||
app.get('/api/projections/:socCode', (req, res) => {
|
||||
const { socCode } = req.params;
|
||||
console.log('Received SOC Code:', socCode);
|
||||
|
||||
const socRow = projectionsData.find(row => row[0] === socCode);
|
||||
|
||||
if (socRow) {
|
||||
const projections = {
|
||||
"SOC Code": socRow[0],
|
||||
"Occupation": socRow[2],
|
||||
"2022 Employment": socRow[3],
|
||||
"2032 Employment": socRow[4],
|
||||
"Total Change": socRow[5],
|
||||
"Annual Openings": socRow[7],
|
||||
"Labor Force Exits": socRow[8],
|
||||
"Projected Growth": socRow[9]
|
||||
};
|
||||
|
||||
res.status(200).json(projections);
|
||||
} else {
|
||||
res.status(404).json({ error: 'SOC Code not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/salary', async (req, res) => {
|
||||
const { socCode, area } = req.query;
|
||||
|
||||
console.log('Received /api/salary request:', { socCode, area });
|
||||
const { state } = req.query;
|
||||
console.log('Projections request for', socCode, ' state=', state);
|
||||
|
||||
if (!socCode) {
|
||||
console.error('Missing required parameters:', { socCode });
|
||||
return res.status(400).json({ error: 'SOC Code is required.' });
|
||||
}
|
||||
|
||||
// If no ?state=, default to "United States"
|
||||
const areaName = state ? state.trim() : 'United States';
|
||||
|
||||
// Find the row for the requested area
|
||||
const rowForState = allProjections.find(
|
||||
(row) =>
|
||||
row['Occupation Code'] === socCode.trim() &&
|
||||
row['Area Name']?.toLowerCase() === areaName.toLowerCase()
|
||||
);
|
||||
|
||||
// Also find the row for "United States"
|
||||
const rowForUS = allProjections.find(
|
||||
(row) =>
|
||||
row['Occupation Code'] === socCode.trim() &&
|
||||
row['Area Name']?.toLowerCase() === 'united states'
|
||||
);
|
||||
|
||||
if (!rowForState && !rowForUS) {
|
||||
return res.status(404).json({ error: 'No projections found for this SOC + area.' });
|
||||
}
|
||||
|
||||
function formatRow(r) {
|
||||
if (!r) return null;
|
||||
return {
|
||||
area: r['Area Name'],
|
||||
baseYear: r['Base Year'],
|
||||
base: r['Base'],
|
||||
projectedYear: r['Projected Year'],
|
||||
projection: r['Projection'],
|
||||
change: r['Change'],
|
||||
percentChange: r['Percent Change'],
|
||||
annualOpenings: r['Average Annual Openings'],
|
||||
occupationName: r['Occupation Name']
|
||||
};
|
||||
}
|
||||
|
||||
const result = {
|
||||
state: formatRow(rowForState),
|
||||
national: formatRow(rowForUS)
|
||||
};
|
||||
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
/**************************************************
|
||||
* Salary route
|
||||
**************************************************/
|
||||
app.get('/api/salary', async (req, res) => {
|
||||
const { socCode, area } = req.query;
|
||||
console.log('Received /api/salary request:', { socCode, area });
|
||||
if (!socCode) {
|
||||
return res.status(400).json({ error: 'SOC Code is required' });
|
||||
}
|
||||
|
||||
// Query for regional salary data
|
||||
// Query for regional
|
||||
const regionalQuery = `
|
||||
SELECT A_PCT10 AS regional_PCT10, A_PCT25 AS regional_PCT25,
|
||||
A_MEDIAN AS regional_MEDIAN, A_PCT75 AS regional_PCT75,
|
||||
SELECT A_PCT10 AS regional_PCT10, A_PCT25 AS regional_PCT25,
|
||||
A_MEDIAN AS regional_MEDIAN, A_PCT75 AS regional_PCT75,
|
||||
A_PCT90 AS regional_PCT90
|
||||
FROM salary_data
|
||||
WHERE OCC_CODE = ? AND AREA_TITLE = ?
|
||||
`;
|
||||
|
||||
// Query for national salary data (updated AREA_TITLE = 'U.S.')
|
||||
// Query for national
|
||||
const nationalQuery = `
|
||||
SELECT A_PCT10 AS national_PCT10, A_PCT25 AS national_PCT25,
|
||||
A_MEDIAN AS national_MEDIAN, A_PCT75 AS national_PCT75,
|
||||
SELECT A_PCT10 AS national_PCT10, A_PCT25 AS national_PCT25,
|
||||
A_MEDIAN AS national_MEDIAN, A_PCT75 AS national_PCT75,
|
||||
A_PCT90 AS national_PCT90
|
||||
FROM salary_data
|
||||
WHERE OCC_CODE = ? AND AREA_TITLE = 'U.S.'
|
||||
`;
|
||||
|
||||
try {
|
||||
let regionalRow = null;
|
||||
let nationalRow = null;
|
||||
|
||||
if (area) {
|
||||
regionalRow = await db.get(regionalQuery, [socCode, area]);
|
||||
}
|
||||
nationalRow = await db.get(nationalQuery, [socCode]);
|
||||
|
||||
if (!regionalRow && !nationalRow) {
|
||||
console.log('No salary data found for:', { socCode, area });
|
||||
return res.status(404).json({ error: 'No salary data found for the given SOC Code' });
|
||||
return res.status(404).json({ error: 'No salary data found' });
|
||||
}
|
||||
|
||||
const salaryData = {
|
||||
regional: regionalRow || {},
|
||||
national: nationalRow || {}
|
||||
};
|
||||
|
||||
console.log('Salary data retrieved:', salaryData);
|
||||
res.json(salaryData);
|
||||
} catch (error) {
|
||||
console.error('Error executing query:', error.message);
|
||||
console.error('Error executing salary query:', error.message);
|
||||
res.status(500).json({ error: 'Failed to fetch salary data' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Route to fetch job zones and check for missing salary data
|
||||
/**************************************************
|
||||
* job-zones route
|
||||
**************************************************/
|
||||
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);
|
||||
// Format them
|
||||
const formattedSocCodes = socCodes.map((code) => {
|
||||
let cleaned = code.trim().replace(/\./g, '');
|
||||
if (!cleaned.includes('-') && cleaned.length === 6) {
|
||||
cleaned = cleaned.slice(0, 2) + '-' + cleaned.slice(2);
|
||||
}
|
||||
return cleanedCode.slice(0, 7); // Keep first 7 characters
|
||||
return cleaned.slice(0, 7);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const placeholders = formattedSocCodes.map(() => '?').join(',');
|
||||
const q = `
|
||||
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(q, formattedSocCodes);
|
||||
console.log("Salary Data Query Results:", rows);
|
||||
const jobZoneMapping = rows.reduce((acc, row) => {
|
||||
const isMissing = [row.A_MEDIAN, row.A_PCT10, row.A_PCT25, row.A_PCT75]
|
||||
.some((v) => !v || v === '#' || v === '*');
|
||||
acc[row.OCC_CODE] = {
|
||||
job_zone: row.JOB_ZONE,
|
||||
limited_data: isMissing ? 1 : 0
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
console.log("Job Zone & Limited Data:", 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
|
||||
/**************************************************
|
||||
* user-profile by ID route
|
||||
**************************************************/
|
||||
app.get('/api/user-profile/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: 'Profile ID is required' });
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Start the Express server
|
||||
/**************************************************
|
||||
* Start the Express server
|
||||
**************************************************/
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on https://34.16.120.118:${PORT}`);
|
||||
});
|
||||
|
468951
public/economicproj.json
Normal file
468951
public/economicproj.json
Normal file
File diff suppressed because it is too large
Load Diff
20036
public/economicprojnatl.json
Normal file
20036
public/economicprojnatl.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,66 @@ import './Dashboard.css';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { fetchSchools } from '../utils/apiUtils.js';
|
||||
|
||||
const STATES = [
|
||||
{ name: 'Alabama', code: 'AL' },
|
||||
{ name: 'Alaska', code: 'AK' },
|
||||
{ name: 'Arizona', code: 'AZ' },
|
||||
{ name: 'Arkansas', code: 'AR' },
|
||||
{ name: 'California', code: 'CA' },
|
||||
{ name: 'Colorado', code: 'CO' },
|
||||
{ name: 'Connecticut', code: 'CT' },
|
||||
{ name: 'Delaware', code: 'DE' },
|
||||
{ name: 'District of Columbia', code: 'DC' },
|
||||
{ name: 'Florida', code: 'FL' },
|
||||
{ name: 'Georgia', code: 'GA' },
|
||||
{ name: 'Hawaii', code: 'HI' },
|
||||
{ name: 'Idaho', code: 'ID' },
|
||||
{ name: 'Illinois', code: 'IL' },
|
||||
{ name: 'Indiana', code: 'IN' },
|
||||
{ name: 'Iowa', code: 'IA' },
|
||||
{ name: 'Kansas', code: 'KS' },
|
||||
{ name: 'Kentucky', code: 'KY' },
|
||||
{ name: 'Louisiana', code: 'LA' },
|
||||
{ name: 'Maine', code: 'ME' },
|
||||
{ name: 'Maryland', code: 'MD' },
|
||||
{ name: 'Massachusetts', code: 'MA' },
|
||||
{ name: 'Michigan', code: 'MI' },
|
||||
{ name: 'Minnesota', code: 'MN' },
|
||||
{ name: 'Mississippi', code: 'MS' },
|
||||
{ name: 'Missouri', code: 'MO' },
|
||||
{ name: 'Montana', code: 'MT' },
|
||||
{ name: 'Nebraska', code: 'NE' },
|
||||
{ name: 'Nevada', code: 'NV' },
|
||||
{ name: 'New Hampshire', code: 'NH' },
|
||||
{ name: 'New Jersey', code: 'NJ' },
|
||||
{ name: 'New Mexico', code: 'NM' },
|
||||
{ name: 'New York', code: 'NY' },
|
||||
{ name: 'North Carolina', code: 'NC' },
|
||||
{ name: 'North Dakota', code: 'ND' },
|
||||
{ name: 'Ohio', code: 'OH' },
|
||||
{ name: 'Oklahoma', code: 'OK' },
|
||||
{ name: 'Oregon', code: 'OR' },
|
||||
{ name: 'Pennsylvania', code: 'PA' },
|
||||
{ name: 'Rhode Island', code: 'RI' },
|
||||
{ name: 'South Carolina', code: 'SC' },
|
||||
{ name: 'South Dakota', code: 'SD' },
|
||||
{ name: 'Tennessee', code: 'TN' },
|
||||
{ name: 'Texas', code: 'TX' },
|
||||
{ name: 'Utah', code: 'UT' },
|
||||
{ name: 'Vermont', code: 'VT' },
|
||||
{ name: 'Virginia', code: 'VA' },
|
||||
{ name: 'Washington', code: 'WA' },
|
||||
{ name: 'West Virginia', code: 'WV' },
|
||||
{ name: 'Wisconsin', code: 'WI' },
|
||||
{ name: 'Wyoming', code: 'WY' },
|
||||
];
|
||||
|
||||
// 2) Helper to convert state code => full name
|
||||
function getFullStateName(code) {
|
||||
const found = STATES.find((s) => s.code === code?.toUpperCase());
|
||||
return found ? found.name : '';
|
||||
}
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||
|
||||
function Dashboard() {
|
||||
@ -308,10 +368,17 @@ function Dashboard() {
|
||||
salaryResponse = { data: {} };
|
||||
}
|
||||
|
||||
const fullName = getFullStateName(userState);
|
||||
|
||||
// Economic
|
||||
let economicResponse;
|
||||
try {
|
||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`);
|
||||
economicResponse = await axios.get(
|
||||
`${apiUrl}/projections/${socCode.split('.')[0]}`,
|
||||
{
|
||||
params: { state: fullName }, // e.g. "Kentucky"
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
economicResponse = { data: {} };
|
||||
}
|
||||
|
@ -1,70 +1,121 @@
|
||||
// EconomicProjections (in EconomicProjections.js)
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
function EconomicProjections({ socCode }) {
|
||||
const [projections, setProjections] = useState(null);
|
||||
/**
|
||||
* Props:
|
||||
* - socCode: e.g. "17-1011"
|
||||
* - stateName: e.g. "Kentucky" (optional; if not provided, defaults to "United States")
|
||||
*/
|
||||
function EconomicProjections({ socCode, stateName }) {
|
||||
const [data, setData] = useState(null); // { state: {...}, national: {...} }
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (socCode) {
|
||||
const cleanedSocCode = socCode.split('.')[0]; // Clean the SOC code
|
||||
console.log(`Fetching economic projections for cleaned SOC code: ${cleanedSocCode}`);
|
||||
if (!socCode) return;
|
||||
|
||||
const fetchProjections = async () => {
|
||||
try {
|
||||
// Load the Excel file from public folder
|
||||
const response = await fetch('/public/ltprojections.xlsx');
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Assuming data is in the first sheet
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const data = XLSX.utils.sheet_to_json(sheet);
|
||||
|
||||
// Find the projections matching the SOC code
|
||||
const filteredProjections = data.find(
|
||||
(row) => row['SOC Code'] === cleanedSocCode
|
||||
);
|
||||
|
||||
if (filteredProjections) {
|
||||
setProjections(filteredProjections);
|
||||
} else {
|
||||
throw new Error('No data found for the given SOC code.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error loading economic projections.');
|
||||
console.error('Projections Fetch Error:', err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
try {
|
||||
const encodedState = stateName ? encodeURIComponent(stateName) : '';
|
||||
const url = `/api/projections/${socCode}?state=${encodedState}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Server responded with status ${res.status}`);
|
||||
}
|
||||
};
|
||||
const json = await res.json();
|
||||
setData(json);
|
||||
} catch (err) {
|
||||
console.error("Error fetching economic projections:", err);
|
||||
setError("Failed to load economic projections.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjections();
|
||||
fetchData();
|
||||
}, [socCode, stateName]);
|
||||
|
||||
if (loading) return <div>Loading Projections...</div>;
|
||||
if (error) return <div style={{ color: 'red' }}>{error}</div>;
|
||||
if (!data) return null;
|
||||
|
||||
const { state, national } = data;
|
||||
|
||||
/**
|
||||
* Safely parse the value to a number. If parsing fails, we just return the original string.
|
||||
* Then we format with toLocaleString() if numeric.
|
||||
*/
|
||||
const formatNumber = (val) => {
|
||||
if (val == null || val === '') return 'N/A';
|
||||
|
||||
// Coerce string -> number
|
||||
const parsed = Number(val);
|
||||
if (!isNaN(parsed)) {
|
||||
// If it’s actually numeric
|
||||
return parsed.toLocaleString();
|
||||
} else {
|
||||
// If it’s truly not numeric, return the raw value
|
||||
return val;
|
||||
}
|
||||
}, [socCode]);
|
||||
|
||||
if (error) {
|
||||
return <div className="error">{error}</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading-spinner">Loading...</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="economic-projections">
|
||||
<h3>Economic Projections for {projections['Occupation Title'] || 'Unknown Occupation'}</h3>
|
||||
<ul>
|
||||
<li>2022 Employment: {projections['2022 Employment']}</li>
|
||||
<li>2032 Employment: {projections['2032 Employment']}</li>
|
||||
<li>Total Change: {projections['Total Change']}</li>
|
||||
<li>Annual Openings: {projections['Annual Openings']}</li>
|
||||
<li>Labor Force Exits: {projections['Labor Force Exits']}</li>
|
||||
<li>Projected Growth: {projections['Projected Growth']}</li>
|
||||
</ul>
|
||||
<div className="border p-2 my-3">
|
||||
<h3 className="text-lg font-bold mb-3">
|
||||
Economic Projections: {state?.occupationName ?? 'N/A'}
|
||||
</h3>
|
||||
<table className="w-full border text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="p-2 text-left">Metric</th>
|
||||
<th className="p-2 text-left">{state?.area || 'State'}</th>
|
||||
<th className="p-2 text-left">{national?.area || 'U.S.'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="p-2">Base Year</td>
|
||||
<td className="p-2">{state?.baseYear ?? 'N/A'}</td>
|
||||
<td className="p-2">{national?.baseYear ?? 'N/A'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2">Base Employment</td>
|
||||
<td className="p-2">{formatNumber(state?.base)}</td>
|
||||
<td className="p-2">{formatNumber(national?.base)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2">Projected Year</td>
|
||||
<td className="p-2">{state?.projectedYear ?? 'N/A'}</td>
|
||||
<td className="p-2">{national?.projectedYear ?? 'N/A'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2">Projected Employment</td>
|
||||
<td className="p-2">{formatNumber(state?.projection)}</td>
|
||||
<td className="p-2">{formatNumber(national?.projection)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2">Employment Change</td>
|
||||
<td className="p-2">{formatNumber(state?.change)}</td>
|
||||
<td className="p-2">{formatNumber(national?.change)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2">Percent Change</td>
|
||||
<td className="p-2">
|
||||
{state?.percentChange != null ? `${state.percentChange}%` : 'N/A'}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{national?.percentChange != null ? `${national.percentChange}%` : 'N/A'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2">Annual Openings</td>
|
||||
<td className="p-2">{formatNumber(state?.annualOpenings)}</td>
|
||||
<td className="p-2">{formatNumber(national?.annualOpenings)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -271,20 +271,55 @@ function PopoutPanel({
|
||||
|
||||
{/* Economic Projections */}
|
||||
<div className="rounded bg-gray-50 p-4">
|
||||
<h3 className="mb-2 text-base font-medium">
|
||||
Economic Projections for {userState}
|
||||
</h3>
|
||||
{economicProjections && typeof economicProjections === "object" ? (
|
||||
<ul className="space-y-1 text-sm text-gray-700">
|
||||
<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 className="text-sm text-gray-500">
|
||||
No economic projections available.
|
||||
</p>
|
||||
)}
|
||||
{(() => {
|
||||
if (!economicProjections.state && !economicProjections.national) {
|
||||
return (
|
||||
<p className="text-sm text-gray-500">
|
||||
No economic projections available.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, proceed
|
||||
const st = economicProjections.state || {};
|
||||
const nat = economicProjections.national || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-2 text-base font-medium">
|
||||
Economic Projections for {userState} —{' '}
|
||||
{st.occupationName ? st.occupationName : 'N/A'}
|
||||
</h3>
|
||||
|
||||
<table className="w-full text-sm text-gray-700">
|
||||
<thead>
|
||||
<tr className="bg-gray-200">
|
||||
<th className="p-2 text-left">Metric</th>
|
||||
<th className="p-2 text-left">{st.area ?? 'State'}</th>
|
||||
<th className="p-2 text-left">{nat.area ?? 'U.S.'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="p-2 font-medium">2022 Employment</td>
|
||||
<td className="p-2">{st.base ?? 'N/A'}</td>
|
||||
<td className="p-2">{nat.base ?? 'N/A'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2 font-medium">2032 Employment</td>
|
||||
<td className="p-2">{st.projection ?? 'N/A'}</td>
|
||||
<td className="p-2">{nat.projection ?? 'N/A'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-2 font-medium">Annual Openings</td>
|
||||
<td className="p-2">{st.annualOpenings ?? 'N/A'}</td>
|
||||
<td className="p-2">{nat.annualOpenings ?? 'N/A'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Salary Data */}
|
||||
|
@ -94,6 +94,40 @@ function UserProfile() {
|
||||
fetchProfileAndAreas();
|
||||
}, []); // only runs once
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAreasByState = async () => {
|
||||
if (!selectedState) {
|
||||
setAreas([]);
|
||||
return;
|
||||
}
|
||||
setLoadingAreas(true);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const areaRes = await fetch(`/api/areas?state=${selectedState}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!areaRes.ok) {
|
||||
throw new Error('Failed to fetch areas');
|
||||
}
|
||||
|
||||
const areaData = await areaRes.json();
|
||||
setAreas(areaData.areas || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching areas:', error);
|
||||
setAreas([]);
|
||||
} finally {
|
||||
setLoadingAreas(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAreasByState();
|
||||
}, [selectedState]);
|
||||
|
||||
const handleFormSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@ -262,6 +296,8 @@ function UserProfile() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{loadingAreas && <p className="text-sm text-gray-500">Loading areas...</p>}
|
||||
|
||||
{/* Areas Dropdown */}
|
||||
{loadingAreas ? (
|
||||
<p className="text-sm text-gray-500">Loading areas...</p>
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user