dev1/src/components/PopoutPanel.js
2025-05-01 15:21:56 +00:00

471 lines
16 KiB
JavaScript

import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { ClipLoader } from "react-spinners";
import { v4 as uuidv4 } from "uuid";
import LoanRepayment from "./LoanRepayment.js";
import "./PopoutPanel.css"; // You can keep or remove depending on your needs
function PopoutPanel({
isVisible,
data = {},
userState = "N/A",
loading = false,
error = null,
closePanel,
updateChatbotContext,
}) {
// Original local states
const [isCalculated, setIsCalculated] = useState(false);
const [results, setResults] = useState([]);
const [loadingCalculation, setLoadingCalculation] = useState(false);
const [persistedROI, setPersistedROI] = useState({});
const [programLengths, setProgramLengths] = useState([]);
const [sortBy, setSortBy] = useState("tuition");
const [maxTuition, setMaxTuition] = useState(50000);
const [maxDistance, setMaxDistance] = useState(200);
const token = localStorage.getItem("token");
const navigate = useNavigate();
// Destructure your data
const {
jobDescription = null,
tasks = null,
title = "Career Details",
economicProjections = {},
salaryData = [],
schools = [],
} = data || {};
// Clear results if sorting or filters change
useEffect(() => {
setResults([]);
setIsCalculated(false);
}, [sortBy, maxTuition, maxDistance]);
// Derive program lengths from school CREDDESC
useEffect(() => {
setProgramLengths(
schools.map((school) => getProgramLength(school["CREDDESC"]))
);
}, [schools]);
// Update chatbot context if data is present
useEffect(() => {
if (data && Object.keys(data).length > 0) {
updateChatbotContext({
careerDetails: data,
schools,
salaryData,
economicProjections,
results,
persistedROI,
});
}
}, [
data,
schools,
salaryData,
economicProjections,
results,
persistedROI,
updateChatbotContext,
]);
// If panel isn't visible, don't render
if (!isVisible) return null;
// If the panel or the loan calc is loading, show a spinner
if (loading || loadingCalculation) {
return (
<div className="popout-panel fixed top-0 right-0 z-50 h-full w-full max-w-xl overflow-y-auto bg-white shadow-xl">
<div className="p-4">
<button
className="mb-4 rounded bg-red-100 px-2 py-1 text-sm text-red-600 hover:bg-red-200"
onClick={closePanel}
>
X
</button>
<h2 className="mb-2 text-xl font-semibold">Loading Career Details...</h2>
<ClipLoader size={35} color="#4A90E2" />
</div>
</div>
);
}
// Original helper
function 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;
}
// Original close logic
function handleClosePanel() {
setResults([]);
setIsCalculated(false);
closePanel();
}
async function handlePlanMyPath() {
if (!token) {
alert("You need to be logged in to create a career path.");
return;
}
try {
// 1) Fetch existing career profiles (a.k.a. "careerPaths")
const allPathsResponse = await fetch(
`${process.env.REACT_APP_API_URL}/premium/career-profile/all`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}
);
if (!allPathsResponse.ok) {
throw new Error(`HTTP error ${allPathsResponse.status}`);
}
// The server returns { careerPaths: [...] }
const { careerPaths } = await allPathsResponse.json();
// 2) Check if there's already a career path with the same name
const match = careerPaths.find((cp) => cp.career_name === data.title);
if (match) {
// If a path already exists for this career, confirm with the user
const decision = window.confirm(
`A career path (scenario) for "${data.title}" already exists.\n\n` +
`Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
);
if (decision) {
// Reload existing path → go to Paywall
navigate("/paywall", {
state: {
selectedCareer: {
career_path_id: match.id, // 'id' is the primary key from the DB
career_name: data.title,
},
},
});
return;
}
}
// 3) Otherwise, create a new career profile using POST /premium/career-profile
const newResponse = await fetch(
`${process.env.REACT_APP_API_URL}/premium/career-profile`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
// The server expects at least career_name
career_name: data.title,
// Optionally pass scenario_title, start_date, etc.
}),
}
);
if (!newResponse.ok) {
throw new Error("Failed to create new career path.");
}
// The server returns something like { message: 'Career profile upserted.', career_path_id: 'xxx-xxx' }
const result = await newResponse.json();
const newlyCreatedId = result?.career_path_id;
// 4) Navigate to /paywall, passing the newly created career_path_id
navigate("/paywall", {
state: {
selectedCareer: {
career_path_id: newlyCreatedId,
career_name: data.title,
},
},
});
} catch (error) {
console.error("Error in Plan My Path:", error);
}
}
// Filter & sort schools
const filteredAndSortedSchools = [...schools]
.filter((school) => {
const inStateCost = parseFloat(school["In_state cost"]) || 0;
const distance = parseFloat((school["distance"] || "0").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") {
const distA = parseFloat((a["distance"] || "0").replace(" mi", ""));
const distB = parseFloat((b["distance"] || "0").replace(" mi", ""));
return distA - distB;
}
return 0;
});
return (
<div className="popout-panel fixed top-0 right-0 z-50 flex h-full w-full max-w-xl flex-col bg-white shadow-xl">
{/* Header */}
<div className="flex items-center justify-between border-b p-4">
<button
className="rounded bg-red-100 px-2 py-1 text-sm text-red-600 hover:bg-red-200"
onClick={handleClosePanel}
>
X
</button>
<button
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
onClick={handlePlanMyPath}
>
Plan My Path
</button>
</div>
{/* Main Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-6">
{/* Title */}
<h2 className="text-xl font-semibold">{title}</h2>
{/* Error */}
{error && (
<div className="rounded bg-red-50 p-2 text-red-600">
{error}
</div>
)}
{/* Job Description */}
<div className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Job Description</h3>
<p className="text-sm text-gray-700">
{jobDescription || "No description available"}
</p>
</div>
{/* Tasks */}
<div className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Expected Tasks</h3>
{tasks && tasks.length > 0 ? (
<ul className="list-disc space-y-1 pl-5 text-sm text-gray-700">
{tasks.map((task, idx) => (
<li key={idx}>{task}</li>
))}
</ul>
) : (
<p className="text-sm text-gray-500">No tasks available.</p>
)}
</div>
{/* 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>
)}
</div>
{/* Salary Data */}
<div className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Salary Data</h3>
{salaryData.length > 0 ? (
<table className="w-full text-sm text-gray-700">
<thead>
<tr className="bg-gray-200 text-left">
<th className="p-2">Percentile</th>
<th className="p-2">Regional Salary</th>
<th className="p-2">US Salary</th>
</tr>
</thead>
<tbody>
{salaryData.map((point, idx) => (
<tr key={idx} className="border-b">
<td className="p-2">{point.percentile}</td>
<td className="p-2">
{point.regionalSalary > 0
? `$${parseInt(point.regionalSalary, 10).toLocaleString()}`
: "N/A"}
</td>
<td className="p-2">
{point.nationalSalary > 0
? `$${parseInt(point.nationalSalary, 10).toLocaleString()}`
: "N/A"}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-sm text-gray-500">
Salary data is not available.
</p>
)}
</div>
{/* Schools Offering Programs */}
<div>
<h3 className="mb-2 text-base font-medium">Schools Offering Programs</h3>
{/* Filter Bar */}
<div className="mb-4 flex items-center space-x-4">
<label className="text-sm text-gray-600">
Sort:
<select
className="ml-2 rounded border px-2 py-1 text-sm"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="tuition">Tuition</option>
<option value="distance">Distance</option>
</select>
</label>
<label className="text-sm text-gray-600">
Tuition (max):
<input
type="number"
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxTuition}
onChange={(e) => setMaxTuition(Number(e.target.value))}
/>
</label>
<label className="text-sm text-gray-600">
Distance (max):
<input
type="number"
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxDistance}
onChange={(e) => setMaxDistance(Number(e.target.value))}
/>
</label>
</div>
{filteredAndSortedSchools.length > 0 ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{filteredAndSortedSchools.map((school, idx) => (
<div key={idx} className="rounded border p-3 text-sm">
<strong>{school["INSTNM"] || "Unnamed School"}</strong>
<p>Degree Type: {school["CREDDESC"] || "N/A"}</p>
<p>In-State Tuition: ${school["In_state cost"] || "N/A"}</p>
<p>Out-of-State Tuition: ${school["Out_state cost"] || "N/A"}</p>
<p>Distance: {school["distance"] || "N/A"}</p>
<p>
Website:{" "}
<a
href={school["Website"]}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{school["Website"]}
</a>
</p>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500">
No schools matching your filters.
</p>
)}
</div>
{/* Loan Repayment Analysis */}
<section className="rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">Loan Repayment Analysis</h3>
<LoanRepayment
schools={filteredAndSortedSchools.map((school, i) => ({
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[i],
}))}
salaryData={salaryData}
setResults={setResults}
setLoading={setLoadingCalculation}
setPersistedROI={setPersistedROI}
/>
</section>
{/* Results Display */}
{results.length > 0 && (
<div className="results-container rounded bg-gray-50 p-4">
<h3 className="mb-2 text-base font-medium">
Comparisons by School over the life of the loan
</h3>
{/*
=========================================
Here's the key part: a grid for results.
=========================================
*/}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{results.map((result, idx) => (
<div
key={idx}
className="rounded border p-3 text-sm text-gray-700"
>
<h4 className="mb-1 text-sm font-medium">
{result.schoolName} - {result.degreeType || "N/A"}
</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={
parseFloat(result.netGain) < 0
? "text-red-600"
: "text-green-600"
}
>
Net Gain: ${result.netGain}
</p>
<p>Monthly Salary (Gross): ${result.monthlySalary}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
export default PopoutPanel;