294 lines
10 KiB
JavaScript
294 lines
10 KiB
JavaScript
import { ClipLoader } from 'react-spinners';
|
|
import LoanRepayment from './LoanRepayment.js';
|
|
import SchoolFilters from './SchoolFilters';
|
|
import './PopoutPanel.css';
|
|
import { useState, useEffect } from 'react';
|
|
|
|
function PopoutPanel({
|
|
isVisible,
|
|
data = {},
|
|
userState = 'N/A', // Passed explicitly from Dashboard
|
|
loading = false,
|
|
error = null,
|
|
closePanel,
|
|
updateChatbotContext,
|
|
}) {
|
|
const [isCalculated, setIsCalculated] = useState(false);
|
|
const [results, setResults] = useState([]); // Store loan repayment calculation results
|
|
const [loadingCalculation, setLoadingCalculation] = useState(false);
|
|
const [persistedROI, setPersistedROI] = useState({});
|
|
const [programLengths, setProgramLengths] = useState([]);
|
|
const [sortBy, setSortBy] = useState('tuition'); // Default sorting
|
|
const [maxTuition, setMaxTuition] = useState(50000); // Set default max tuition value
|
|
const [maxDistance, setMaxDistance] = useState(200); // Set default max distance value
|
|
|
|
const {
|
|
jobDescription = null,
|
|
tasks = null,
|
|
title = 'Career Details',
|
|
economicProjections = {},
|
|
salaryData = [],
|
|
schools = [],
|
|
} = data || {};
|
|
|
|
useEffect(() => {
|
|
setResults([]);
|
|
setIsCalculated(false);
|
|
}, [sortBy, maxTuition, maxDistance]); // Ensure no other dependencies!
|
|
|
|
useEffect(() => {
|
|
setProgramLengths(schools.map(school => getProgramLength(school['CREDDESC'])));
|
|
}, [schools]);
|
|
|
|
useEffect(() => {
|
|
console.log("📩 Updating Chatbot Context from PopoutPanel:", data);
|
|
|
|
if (data && Object.keys(data).length > 0) {
|
|
updateChatbotContext({
|
|
careerDetails: data,
|
|
schools,
|
|
salaryData,
|
|
economicProjections,
|
|
results,
|
|
persistedROI, // ✅ Make sure ROI is included!
|
|
});
|
|
} else {
|
|
console.log("⚠️ No valid PopoutPanel data to update chatbot context.");
|
|
}
|
|
}, [data, schools, salaryData, economicProjections, results, persistedROI, updateChatbotContext]);
|
|
|
|
|
|
if (!isVisible) return null;
|
|
|
|
if (loading || loadingCalculation) {
|
|
return (
|
|
<div className="popout-panel">
|
|
<button className="close-btn" onClick={closePanel}>X</button>
|
|
<h2>Loading Career Details...</h2>
|
|
<ClipLoader size={35} color="#4A90E2" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
// Get program length for calculating tuition
|
|
const getProgramLength = (degreeType) => {
|
|
if (degreeType?.includes("Associate")) return 2;
|
|
if (degreeType?.includes("Bachelor")) return 4;
|
|
if (degreeType?.includes("Master")) return 6;
|
|
if (degreeType?.includes("Doctoral") || degreeType?.includes("First Professional")) return 8;
|
|
if (degreeType?.includes("Certificate")) return 1;
|
|
return 4; // Default to 4 years if unspecified
|
|
};
|
|
|
|
function handleClosePanel() {
|
|
setResults([]); // Clear only LoanRepayment results
|
|
setIsCalculated(false); // Reset calculation state
|
|
closePanel(); // Maintain existing close behavior
|
|
}
|
|
|
|
|
|
/** 🔹 Apply Sorting & Filtering Directly at Render Time **/
|
|
const filteredAndSortedSchools = [...schools]
|
|
.filter(school => {
|
|
const inStateCost = parseFloat(school['In_state cost']);
|
|
const distance = parseFloat(school['distance'].replace(' mi', ''));
|
|
|
|
return (
|
|
inStateCost <= maxTuition &&
|
|
distance <= maxDistance
|
|
);
|
|
})
|
|
.sort((a, b) => {
|
|
if (sortBy === 'tuition') return a['In_state cost'] - b['In_state cost'];
|
|
if (sortBy === 'distance') return a['distance'] - b['distance'];
|
|
return 0;
|
|
});
|
|
|
|
return (
|
|
<div className="popout-panel">
|
|
{/* Header with Close & Plan My Path Buttons */}
|
|
<div className="panel-header">
|
|
<button className="close-btn" onClick={closePanel}>X</button>
|
|
<button className="plan-path-btn">Plan My Path</button>
|
|
</div>
|
|
|
|
<h2>{title}</h2>
|
|
|
|
{/* Job Description and Tasks */}
|
|
<div className="job-description">
|
|
<h3>Job Description</h3>
|
|
<p>{jobDescription || 'No description available'}</p>
|
|
</div>
|
|
|
|
<div className="job-tasks">
|
|
<h3>Expected Tasks</h3>
|
|
{tasks && tasks.length > 0 ? (
|
|
<ul>
|
|
{tasks.map((task, index) => (
|
|
<li key={index}>{task}</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p>No tasks available 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>Regional Salary</th>
|
|
<th>US Salary</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{salaryData.map((point, index) => (
|
|
<tr key={index}>
|
|
<td>{point.percentile}</td>
|
|
<td>{point.regionalSalary > 0 ? `$${parseInt(point.regionalSalary, 10).toLocaleString()}` : 'N/A'}</td>
|
|
<td>{point.nationalSalary > 0 ? `$${parseInt(point.nationalSalary, 10).toLocaleString()}` : 'N/A'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
) : (
|
|
<p>Salary data is not available.</p>
|
|
)}
|
|
</div>
|
|
|
|
|
|
{/* Schools Offering Programs Section */}
|
|
<h3>Schools Offering Programs</h3>
|
|
|
|
<div className="schools-offering-container">
|
|
{/* Header and Filters - Not part of grid */}
|
|
<div className="schools-header" style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
marginBottom: '10px',
|
|
justifyContent: 'center',
|
|
alignItems: 'center', }}>
|
|
|
|
<div className="compact-filters" style={{ display: 'flex', gap: '15px', justifyContent: 'center', alignItems: 'center' }}>
|
|
<label>
|
|
Sort:
|
|
<select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value)}
|
|
style={{ marginLeft: '5px', padding: '2px', width: '100px' }}
|
|
>
|
|
<option value="tuition">Tuition</option>
|
|
<option value="distance">Distance</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label>
|
|
Tuition (max):
|
|
<input
|
|
type="number"
|
|
value={maxTuition}
|
|
step={1000}
|
|
min={0}
|
|
max={100000}
|
|
style={{ width: '90px', padding: '2px' }}
|
|
onChange={(e) => setMaxTuition(Number(e.target.value))}
|
|
/>
|
|
</label>
|
|
|
|
<label>
|
|
Distance (max mi):
|
|
<input
|
|
type="number"
|
|
value={maxDistance}
|
|
step={10}
|
|
min={10}
|
|
max={500}
|
|
style={{ width: '70px', padding: '2px' }}
|
|
onChange={(e) => setMaxDistance(Number(e.target.value))}
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div className="schools-offering">
|
|
{filteredAndSortedSchools.length > 0 ? (
|
|
filteredAndSortedSchools.map((school, index) => (
|
|
<div key={index} className="school-card">
|
|
<div><strong>{school['INSTNM']}</strong></div>
|
|
<div>Degree Type: {school['CREDDESC'] || 'Degree type not available for this program'}</div>
|
|
<div>In-State Tuition: ${school['In_state cost'] || 'Tuition not available for this school'}</div>
|
|
<div>Out-of-State Tuition: ${school['Out_state cost'] || 'Tuition 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>
|
|
</div>
|
|
|
|
{/* Loan Repayment Analysis */}
|
|
<h3>Loan Repayment Analysis</h3>
|
|
<LoanRepayment
|
|
schools={filteredAndSortedSchools.map((school, index) => ({
|
|
schoolName: school['INSTNM'],
|
|
inState: parseFloat(school['In_state cost']) || 0,
|
|
outOfState: parseFloat(school['Out_state cost']) || 0,
|
|
inStateGraduate: parseFloat(school['In State Graduate']) || parseFloat(school['In_state cost']) || 0,
|
|
outStateGraduate: parseFloat(school['Out State Graduate']) || parseFloat(school['Out_state cost']) || 0,
|
|
degreeType: school['CREDDESC'],
|
|
programLength: programLengths[index],
|
|
}))}
|
|
salaryData={salaryData}
|
|
setResults={setResults}
|
|
setLoading={setLoadingCalculation}
|
|
setPersistedROI={setPersistedROI} // ✅ Store ROI after calculation
|
|
/>
|
|
|
|
|
|
{/* Results Display */}
|
|
{results.length > 0 && (
|
|
<div className="results-container">
|
|
<h3>Comparisons by School over the life of the loan</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.totalTuition}</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>
|
|
);
|
|
}
|
|
|
|
export default PopoutPanel;
|