url changes for prod and dev

This commit is contained in:
Josh 2024-12-28 20:59:08 +00:00
parent 9696943f9b
commit b3cb86001f
11 changed files with 3764282 additions and 224 deletions

8
.env.production Normal file
View File

@ -0,0 +1,8 @@
ONET_USERNAME=aptivaai
ONET_PASSWORD=2296ahq
REACT_APP_BLS_API_KEY=80d7a65a809a43f3a306a41ec874d231
GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20
COLLEGE_SCORECARD_KEY = BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj
REACT_APP_API_URL=/api

BIN
Institution_data.xlsx Normal file

Binary file not shown.

View File

@ -8,6 +8,8 @@ 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 sqlite3 from 'sqlite3';
import fs from 'fs';
dotenv.config({ path: path.resolve('/home/jcoakley/aptiva-dev1-app/.env') }); // Adjust the path based on your folder structure
console.log('ONET_USERNAME:', process.env.ONET_USERNAME);
@ -18,6 +20,8 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'https://dev.aptivaai.com'];
const mappingFilePath = '/home/jcoakley/aptiva-dev1-app/public/CIP_to_ONET_SOC.xlsx'
const institutionJsonFilePath = path.join(__dirname, '../public/Institution_data.json');
const app = express();
const PORT = process.env.PORT || 5001;
@ -65,7 +69,7 @@ app.use((req, res, next) => {
'Authorization, Content-Type, Accept, Origin, X-Requested-With, Access-Control-Allow-Methods'
);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
} else if (req.path.includes('CIP_institution_mapping_fixed.json')) {
} else if (req.path.includes('Institution_data')) {
// Handle static JSON file CORS
res.setHeader('Access-Control-Allow-Origin', '*'); // Allow all origins for static file
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
@ -348,48 +352,72 @@ app.get('/api/cip/:socCode', (req, res) => {
// Filtered schools endpoint
app.get('/api/schools', (req, res) => {
const { cipCode, state, level, type } = req.query;
const filePath = path.join(__dirname, 'CIP_institution_mapping_fixed.json');
const { cipCode, state } = req.query;
console.log('Query Params:', { cipCode, state });
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return res.status(500).json({ error: 'Failed to load data' });
}
try {
// Load Institution Data from JSON file
const schoolsData = JSON.parse(fs.readFileSync(institutionJsonFilePath, 'utf8'));
console.log('Loaded Schools Data:', schoolsData.length);
// Filter schools based on CIP Code and State
const filteredData = schoolsData.filter((school) => {
const cipCodeValue = school['CIP Code']?.toString().replace('.', '').slice(0, 4) || '';
const formattedCipCode = cipCode?.toString().replace('.', '').slice(0, 4) || '';
console.log('Comparing CIP Code:', cipCodeValue, 'with', formattedCipCode);
console.log('Comparing State:', school['State'], 'with', state);
return (
(!formattedCipCode || cipCodeValue.startsWith(formattedCipCode)) &&
(!state || school['State'] === state)
);
});
console.log('Filtered Schools Data:', filteredData.length);
res.json(filteredData);
} catch (error) {
console.error('Error loading or filtering schools data:', error.message);
res.status(500).json({ error: 'Failed to load or filter schools data.' });
}
});
// Route to handle fetching tuition data using CIP code from College Scorecard API
app.get('/api/tuition/:cipCode', async (req, res) => {
const { cipCode } = req.params;
const { state } = req.query;
console.log(`Received CIP Code: ${cipCode} for state: ${state}`);
app.get('/api/tuition/:cipCode', (req, res) => {
const { cipCode, state } = req.query;
console.log(`Received CIP Code: ${cipCode}, State: ${state}`);
try {
const apiKey = process.env.COLLEGE_SCORECARD_KEY;
const url = `https://api.data.gov/ed/collegescorecard/v1/schools?api_key=${apiKey}&programs.cip_4_digit.code=${cipCode}&school.state=${state}&fields=id,school.name,latest.cost.tuition.in_state,latest.cost.tuition.out_of_state`;
console.log(`Constructed URL: ${url}`);
const workbook = xlsx.readFile(institutionFilePath); // Load Excel file
const sheet = workbook.Sheets[workbook.SheetNames[0]]; // First sheet
const schoolsData = xlsx.utils.sheet_to_json(sheet); // Convert to JSON
const response = await axios.get(url);
const data = response.data.results;
console.log('Loaded Tuition Data:', schoolsData.length);
// Filter data by CIP Code and State
const filteredData = schoolsData.filter((school) => {
const cipCodeValue = school['CIP Code']?.toString().replace(/[^0-9]/g, ''); // Strip non-digits
const stateValue = school['State']?.toUpperCase().trim(); // Normalize state
console.log('College Scorecard API Response:', data); // Log the API response
if (data?.length > 0) {
res.status(200).json(data);
} else {
res.status(404).json({ error: 'No tuition data found for the given CIP code and state.' });
}
} catch (error) {
res.status(500).json({
error: 'Failed to fetch tuition data from College Scorecard API',
details: error.response?.data || error.message
console.log('CIP:', cipCodeValue, 'State:', stateValue); // Debug log each match attempt
console.log('Sample Records:');
console.log(schoolsData.slice(0, 5)); // Logs first 5 rows for debugging
return (
(!cipCode || cipCodeValue === cipCode) && // Exact CIP code match
(!state || stateValue === state.toUpperCase().trim()) // Exact state match
);
});
console.log('Filtered Tuition Data Count:', filteredData.length);
res.json(filteredData); // Send the filtered tuition data
} catch (error) {
console.error('Error reading tuition data:', error);
res.status(500).json({ error: 'Failed to load tuition data.' });
}
});
// Route to handle fetching economic projections for SOC code
app.get('/api/projections/:socCode', (req, res) => {
const { socCode } = req.params;

3764081
public/Institution_data.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,8 @@ function Dashboard() {
const [areaTitle, setAreaTitle] = useState(null);
const [riaSecDescriptions, setRiaSecDescriptions] = useState([]);
const apiUrl = process.env.REACT_APP_API_URL;
// Dynamic API URL
const apiUrl = process.env.REACT_APP_API_URL || '/api';
useEffect(() => {
let descriptions = []; // Declare outside for scope accessibility
@ -49,7 +50,7 @@ function Dashboard() {
const fetchUserProfile = async () => {
try {
const token = localStorage.getItem('token');
const profileResponse = await fetch(`http://localhost:5000/api/user-profile`, {
const profileResponse = await fetch(`${apiUrl}/user-profile`, {
headers: { Authorization: `Bearer ${token}` },
});
@ -70,7 +71,7 @@ function Dashboard() {
};
fetchUserProfile();
}, []);
}, [apiUrl]);
const handleCareerClick = useCallback(
async (career) => {
@ -92,22 +93,24 @@ function Dashboard() {
try {
// Step 1: Fetch CIP Code
const cipResponse = await fetch(`${apiUrl}/api/cip/${socCode}`);
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
const [filteredSchools, economicResponse, tuitionResponse, salaryResponse] = await Promise.all([
fetchSchools(cleanedCipCode, userState),
axios.get(`http://localhost:5001/api/projections/${socCode.split('.')[0]}`),
axios.get(`http://localhost:5001/api/tuition/${cleanedCipCode}`, {
params: { state: userState },
}),
axios.get(`http://localhost:5001/api/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
}),
]);
axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`),
axios.get(`${apiUrl}/tuition/${cleanedCipCode}`, {
params: { state: userState },
}),
axios.get(`${apiUrl}/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
}),
]);
// Step 3: Format Salary Data
const salaryDataPoints = [

View File

@ -7,33 +7,49 @@ function EconomicProjections({ socCode }) {
const [error, setError] = useState(null);
console.log(axios.defaults.baseURL); // Check if baseURL is set
const apiUrl = process.env.REACT_APP_API_URL || '/api';
useEffect(() => {
if (socCode) {
const cleanedSocCode = socCode.split('.')[0]; // Clean the SOC code inside the component
console.log(`Fetching economic projections for cleaned SOC code: ${cleanedSocCode}`);
const fetchProjections = async () => {
const projectionsUrl = process.env.NODE_ENV === 'production'
? '/api/projections/' // In production, routed through Nginx
: 'http://localhost:5001/api/projections/'; // In development, backend is running on localhost:5001
try {
const response = await axios.get(`http://localhost:5001/api/projections/${cleanedSocCode}`);
console.log('Projection Response:', response.data); // Log the projection response
setProjections(response.data); // Set the projections data in state
} catch (err) {
setError('Error fetching economic projections.');
console.error('Error fetching projections:', err);
const projectionsUrl = `${apiUrl}/projections/${cleanedSocCode}`;
// Log URL and SOC code only in development
if (process.env.NODE_ENV !== 'production') {
console.log(`Fetching projections from: ${projectionsUrl}`);
console.log(`Cleaned SOC Code: ${cleanedSocCode}`);
}
try {
const cleanedSocCode = socCode.split('.')[0]; // Clean SOC code
const projectionsUrl = `${apiUrl}/projections/${cleanedSocCode}`;
// Log URL and SOC code only in development
if (process.env.NODE_ENV !== 'production') {
console.log(`Fetching projections from: ${projectionsUrl}`);
console.log(`Cleaned SOC Code: ${cleanedSocCode}`);
}
};
// API call to fetch projections
const response = await axios.get(projectionsUrl);
if (response.status === 200 && response.data) {
setProjections(response.data); // Set projections
if (process.env.NODE_ENV !== 'production') {
console.log('Projections Response:', response.data); // Log data in development
}
} else {
throw new Error('Invalid response from server.');
}
} catch (err) {
setError('Error fetching economic projections.');
console.error('Projections Fetch Error:', err.message);
}
};
fetchProjections();
}
}, [socCode]); // This runs when the socCode prop changes
}, [socCode,apiUrl]); // This runs when the socCode prop changes
if (error) {
return <div className="error">{error}</div>;

View File

@ -18,9 +18,7 @@ const InterestInventory = () => {
setLoading(true); // Start loading
setError(null); // Reset error state
const baseUrl = process.env.REACT_APP_API_URL || 'http://localhost:5001'; // Default to localhost:5001 for development
const url = `${baseUrl}/api/onet/questions?start=1&end=60`; // Make sure the endpoint is correctly appended to the base URL
const url = '/api/onet/questions?start=1&end=60';
try {
const response = await fetch(url, {
method: 'GET',
@ -101,11 +99,8 @@ const InterestInventory = () => {
try {
setIsSubmitting(true);
setError(null); // Clear previous errors
const baseUrl = process.env.NODE_ENV === 'production'
? '/api/onet/submit_answers' // In production, this is proxied by Nginx to server2 (port 5001)
: 'http://localhost:5001/api/onet/submit_answers'; // In development, server2 runs on port 5001
const response = await fetch(baseUrl, {
const url = `${process.env.REACT_APP_API_URL}/onet/submit_answers`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answers }),
@ -165,7 +160,7 @@ const InterestInventory = () => {
))}
</div>
) : (
<p>No questions available. Please try again later.</p>
<p>Loading questions.</p>
)}
<div className="pagination-buttons">

View File

@ -1,28 +1,19 @@
import React, { useState } from 'react';
function LoanRepayment({ tuitionCosts, salaryData, earningHorizon }) {
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)
const [extraPayment, setExtraPayment] = useState(0); // Extra monthly payment
const [results, setResults] = useState(null);
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [sortBy, setSortBy] = useState('monthlyPayment'); // Default sort option
// Validation function
const validateInputs = () => {
console.log('Validating inputs...');
console.log('Tuition Costs:', tuitionCosts);
console.log('Salary Data:', salaryData);
console.log('Tuition Type:', tuitionType);
if (!tuitionCosts || !salaryData || salaryData.length === 0) {
setError('Tuition or salary data is missing.');
return false;
}
if (!tuitionCosts[tuitionType] || tuitionCosts[tuitionType].length === 0) {
setError(`No tuition costs available for ${tuitionType} tuition.`);
if (!schools || schools.length === 0 || !salaryData || salaryData.length === 0) {
setError('School or salary data is missing.');
return false;
}
@ -41,30 +32,23 @@ function LoanRepayment({ tuitionCosts, salaryData, earningHorizon }) {
return false;
}
setError(null); // Clear errors if all inputs are valid
setError(null); // Clear errors if valid
return true;
};
// Loan calculation function
// Loan calculation function for all schools
// Calculate loan details for each school
const calculateLoanDetails = () => {
if (!validateInputs()) return;
setLoading(true);
try {
const tuition = tuitionCosts[tuitionType].reduce((total, year) => total + year, 0);
const schoolResults = schools.map((school) => {
const tuition = tuitionType === 'inState' ? school.inState : school.outOfState;
const monthlyRate = interestRate / 12 / 100;
const loanTermMonths = loanTerm * 12;
console.log('Total Tuition:', tuition);
console.log('Monthly Interest Rate:', monthlyRate);
// Minimum Monthly Payment
// Calculate minimum monthly payment
const minimumMonthlyPayment = tuition * (monthlyRate * Math.pow(1 + monthlyRate, loanTermMonths)) /
(Math.pow(1 + monthlyRate, loanTermMonths) - 1);
console.log('Minimum Monthly Payment:', minimumMonthlyPayment);
// Loan details with extra payments
// Total loan cost with extra payments
const extraMonthlyPayment = minimumMonthlyPayment + extraPayment;
let remainingBalance = tuition;
let monthsWithExtra = 0;
@ -74,59 +58,31 @@ function LoanRepayment({ tuitionCosts, salaryData, earningHorizon }) {
const interest = remainingBalance * monthlyRate;
const principal = extraMonthlyPayment - interest;
remainingBalance -= principal;
if (remainingBalance < 0) remainingBalance = 0;
}
const loanTermWithExtraMonths = monthsWithExtra;
const totalLoanCost = extraMonthlyPayment * monthsWithExtra;
// Total Loan Costs
const totalLoanCostMinimum = minimumMonthlyPayment * loanTermMonths;
const totalLoanCostExtra = extraMonthlyPayment * loanTermWithExtraMonths;
// Calculate net gain
const salary = salaryData[0].value; // 10th percentile salary as default
const totalSalary = salary * earningHorizon;
const netGain = totalSalary - totalLoanCost;
console.log('Total Loan Cost (Minimum Payment):', totalLoanCostMinimum);
console.log('Total Loan Cost (With Extra Payments):', totalLoanCostExtra);
// Results for each salary percentile
const salaryResults = salaryData.map((salaryPoint) => {
let totalSalary = 0;
for (let i = 0; i < earningHorizon; i++) {
totalSalary += salaryPoint.value * Math.pow(1 + salaryPoint.growthRate, i);
}
const netGainMinimum = totalSalary - totalLoanCostMinimum;
const netGainExtra = totalSalary - totalLoanCostExtra;
return {
...salaryPoint,
totalSalary: totalSalary.toFixed(2),
netGainMinimum: netGainMinimum.toFixed(2),
netGainExtra: netGainExtra.toFixed(2),
};
});
setResults({
return {
...school,
tuition,
minimumMonthlyPayment: minimumMonthlyPayment.toFixed(2),
totalLoanCostMinimum: totalLoanCostMinimum.toFixed(2),
loanTermMonths,
extraMonthlyPayment: extraMonthlyPayment.toFixed(2),
totalLoanCostExtra: totalLoanCostExtra.toFixed(2),
loanTermWithExtraMonths,
salaryResults,
});
} catch (error) {
console.error('Calculation Error:', error);
setError('An error occurred during the calculation.');
} finally {
setLoading(false);
}
monthlyPayment: minimumMonthlyPayment.toFixed(2),
totalLoanCost: totalLoanCost.toFixed(2),
netGain: netGain.toFixed(2),
};
});
setResults(schoolResults);
};
return (
<div>
<h2>Loan Repayment and ROI Analysis</h2>
{error && <p style={{ color: 'red' }}>{error}</p>}
<div>
<label>
Tuition Type:
@ -137,67 +93,33 @@ function LoanRepayment({ tuitionCosts, salaryData, earningHorizon }) {
</label>
<label>
Interest Rate (%):
<input
type="number"
value={interestRate}
onChange={(e) => setInterestRate(Number(e.target.value))}
/>
<input type="number" value={interestRate} onChange={(e) => setInterestRate(Number(e.target.value))} />
</label>
<label>
Loan Term (Years):
<input
type="number"
value={loanTerm}
onChange={(e) => setLoanTerm(Number(e.target.value))}
/>
<input type="number" value={loanTerm} onChange={(e) => setLoanTerm(Number(e.target.value))} />
</label>
<label>
Extra Monthly Payment ($):
<input
type="number"
value={extraPayment}
onChange={(e) => setExtraPayment(Number(e.target.value))}
/>
<input type="number" value={extraPayment} onChange={(e) => setExtraPayment(Number(e.target.value))} />
</label>
<button onClick={calculateLoanDetails} disabled={loading}>
{loading ? 'Calculating...' : 'Calculate'}
</button>
</div>
{results && (
{results.length > 0 && (
<div>
<h3>Loan Details</h3>
<p>Total Tuition: ${results.tuition.toFixed(2)}</p>
<p>Minimum Monthly Payment: ${results.minimumMonthlyPayment}</p>
<p>Total Loan Cost (Minimum Payment): ${results.totalLoanCostMinimum}</p>
<p>Loan Term (Minimum Payment): {results.loanTermMonths / 12} years</p>
<p>Extra Monthly Payment: ${results.extraMonthlyPayment}</p>
<p>Total Loan Cost (With Extra Payments): ${results.totalLoanCostExtra}</p>
<p>Loan Term (With Extra Payments): {(results.loanTermWithExtraMonths / 12).toFixed(1)} years</p>
<h3>Net Gain by Salary Percentiles</h3>
<table>
<thead>
<tr>
<th>Percentile</th>
<th>Starting Salary</th>
<th>Total Salary</th>
<th>Net Gain (Min Payment)</th>
<th>Net Gain (Extra Payment)</th>
</tr>
</thead>
<tbody>
{results.salaryResults.map((item, index) => (
<tr key={index}>
<td>{item.percentile}</td>
<td>${item.value.toLocaleString()}</td>
<td>${item.totalSalary}</td>
<td>${item.netGainMinimum}</td>
<td>${item.netGainExtra}</td>
</tr>
))}
</tbody>
</table>
<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 Loan Cost: ${result.totalLoanCost}</p>
<p>Net Gain: ${result.netGain}</p>
</div>
))}
</div>
)}
</div>

View File

@ -1,4 +1,3 @@
// PopoutPanel.js
import { ClipLoader } from 'react-spinners';
import LoanRepayment from './LoanRepayment.js';
import './PopoutPanel.css';
@ -10,7 +9,6 @@ function PopoutPanel({
error = null,
closePanel
}) {
console.log('PopoutPanel Props:', { data, loading, error, userState });
if (loading) {
@ -43,20 +41,9 @@ function PopoutPanel({
);
}
// 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',
description = 'No description available.',
economicProjections = {},
salaryData = [],
schools = [],
@ -76,12 +63,10 @@ function PopoutPanel({
return 4; // Default to 4 years if unspecified
};
console.log('PopoutPanel Props:', { data, schools, salaryData, economicProjections, tuitionData, loading, error, userState });
return (
<div className="popout-panel">
<button onClick={closePanel}>Close</button>
<h2>{data.title || 'Career Details'}</h2>
<p>Description: {data.description || 'No description available.'}</p>
<h2>{title}</h2>
{/* Schools Offering Programs */}
<h3>Schools Offering Programs</h3>
@ -93,7 +78,7 @@ function PopoutPanel({
tuition['school.name']?.toLowerCase().trim() ===
school['Institution Name']?.toLowerCase().trim()
);
return (
<li key={index}>
<strong>{school['Institution Name']}</strong>
@ -111,6 +96,7 @@ function PopoutPanel({
<p>No schools available.</p>
)}
{/* Economic Projections */}
<h3>Economic Projections for {userState}</h3>
{economicProjections && typeof economicProjections === 'object' ? (
@ -151,26 +137,19 @@ function PopoutPanel({
{/* Loan Repayment Analysis */}
{tenthPercentileSalary > 0 && (
<LoanRepayment
tuitionCosts={{
inState: schools.map((school) => {
const matchingTuitionData = tuitionData.find(
(tuition) =>
tuition['school.name']?.toLowerCase().trim() ===
school['Institution Name']?.toLowerCase().trim()
);
const years = getProgramLength(school['CREDDESC']);
return parseFloat(matchingTuitionData?.['latest.cost.tuition_in_state'] * years) || 0;
}),
outOfState: schools.map((school) => {
const matchingTuitionData = tuitionData.find(
(tuition) =>
tuition['school.name']?.toLowerCase().trim() ===
school['Institution Name']?.toLowerCase().trim()
);
const years = getProgramLength(school['CREDDESC']);
return parseFloat(matchingTuitionData?.['latest.cost.tuition_out_of_state'] * years) || 0;
}),
}}
schools={schools.map((school) => {
const matchingTuitionData = tuitionData.find(
(tuition) =>
tuition['school.name']?.toLowerCase().trim() ===
school['Institution Name']?.toLowerCase().trim()
);
const years = getProgramLength(school['CREDDESC']);
return {
schoolName: school['Institution Name'],
inState: parseFloat(matchingTuitionData?.['latest.cost.tuition_in_state'] * years) || 0,
outOfState: parseFloat(matchingTuitionData?.['latest.cost.tuition_out_of_state'] * years) || 0,
};
})}
salaryData={[{ percentile: '10th Percentile', value: tenthPercentileSalary, growthRate: 0.03 }]}
earningHorizon={10}
/>
@ -180,4 +159,3 @@ function PopoutPanel({
}
export default PopoutPanel;

View File

@ -0,0 +1,27 @@
import xlsx from 'xlsx';
import { writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
// Get the directory name
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Define file paths
const excelFilePath = join(__dirname, '../../Institution_data.xlsx');
const jsonFilePath = join(__dirname, 'Institution_data.json');
// Load Excel file
const workbook = xlsx.readFile(excelFilePath);
// Select the first worksheet
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
// Convert worksheet to JSON
const jsonData = xlsx.utils.sheet_to_json(worksheet);
// Write JSON data to file
writeFileSync(jsonFilePath, JSON.stringify(jsonData, null, 2));
console.log('JSON file created successfully at', jsonFilePath);