UI fixes for Dashboard/Popoutpanel/Chatbot
This commit is contained in:
parent
0738457a83
commit
636d33cf94
@ -11,70 +11,74 @@
|
||||
right: 20px; /* Distance from right */
|
||||
z-index: 1000; /* Ensure it appears on top */
|
||||
font-family: Arial, sans-serif; /* Font for consistency */
|
||||
}
|
||||
|
||||
/* Chat Messages */
|
||||
.chat-messages {
|
||||
display: flex; /* For minimized mode */
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease; /* Smooth transition for minimize/maximize */
|
||||
}
|
||||
|
||||
/* Chat Messages */
|
||||
.chat-messages {
|
||||
max-height: 300px; /* Limit height for scrolling */
|
||||
overflow-y: auto; /* Enable vertical scrolling */
|
||||
margin-bottom: 10px; /* Space below the messages */
|
||||
padding-right: 10px; /* Prevent text from touching the edge */
|
||||
}
|
||||
}
|
||||
|
||||
/* Individual Message */
|
||||
.message {
|
||||
/* Individual Message */
|
||||
.message {
|
||||
margin: 5px 0; /* Spacing between messages */
|
||||
padding: 8px 10px; /* Inner padding for readability */
|
||||
border-radius: 6px; /* Rounded message boxes */
|
||||
font-size: 14px; /* Readable font size */
|
||||
line-height: 1.4; /* Comfortable line spacing */
|
||||
}
|
||||
}
|
||||
|
||||
/* User Message */
|
||||
.message.user {
|
||||
/* User Message */
|
||||
.message.user {
|
||||
align-self: flex-end; /* Align user messages to the right */
|
||||
background-color: #007bff; /* Blue background for user */
|
||||
color: #ffffff; /* White text for contrast */
|
||||
}
|
||||
}
|
||||
|
||||
/* Bot Message */
|
||||
.message.bot {
|
||||
/* Bot Message */
|
||||
.message.bot {
|
||||
align-self: flex-start; /* Align bot messages to the left */
|
||||
background-color: #f1f1f1; /* Light gray background for bot */
|
||||
color: #333333; /* Dark text for readability */
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Indicator */
|
||||
.message.bot.typing {
|
||||
/* Loading Indicator */
|
||||
.message.bot.typing {
|
||||
font-style: italic; /* Italic text to indicate typing */
|
||||
color: #666666; /* Subtle color */
|
||||
}
|
||||
}
|
||||
|
||||
/* Chat Input Form */
|
||||
.chat-input-form {
|
||||
/* Chat Input Form */
|
||||
.chat-input-form {
|
||||
display: flex; /* Arrange input and button side by side */
|
||||
gap: 5px; /* Space between input and button */
|
||||
align-items: center; /* Align input and button vertically */
|
||||
}
|
||||
}
|
||||
|
||||
/* Input Field */
|
||||
.chat-input-form input {
|
||||
/* Input Field */
|
||||
.chat-input-form input {
|
||||
flex: 1; /* Take up remaining space */
|
||||
padding: 10px; /* Padding inside input */
|
||||
border: 1px solid #ccc; /* Light gray border */
|
||||
border-radius: 5px; /* Rounded corners */
|
||||
font-size: 14px; /* Font size */
|
||||
}
|
||||
}
|
||||
|
||||
/* Input Focus */
|
||||
.chat-input-form input:focus {
|
||||
/* Input Focus */
|
||||
.chat-input-form input:focus {
|
||||
outline: none; /* Remove blue outline */
|
||||
border-color: #007bff; /* Blue border on focus */
|
||||
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* Glow effect */
|
||||
}
|
||||
}
|
||||
|
||||
/* Send Button */
|
||||
.chat-input-form button {
|
||||
/* Send Button */
|
||||
.chat-input-form button {
|
||||
background-color: #007bff; /* Blue background */
|
||||
color: #ffffff; /* White text */
|
||||
border: none; /* No border */
|
||||
@ -82,30 +86,74 @@
|
||||
border-radius: 5px; /* Rounded corners */
|
||||
cursor: pointer; /* Pointer cursor on hover */
|
||||
font-size: 14px; /* Font size */
|
||||
}
|
||||
}
|
||||
|
||||
/* Send Button Hover */
|
||||
.chat-input-form button:hover {
|
||||
/* Send Button Hover */
|
||||
.chat-input-form button:hover {
|
||||
background-color: #0056b3; /* Darker blue on hover */
|
||||
}
|
||||
}
|
||||
|
||||
/* Send Button Disabled */
|
||||
.chat-input-form button:disabled {
|
||||
/* Send Button Disabled */
|
||||
.chat-input-form button:disabled {
|
||||
background-color: #cccccc; /* Gray background when disabled */
|
||||
cursor: not-allowed; /* Indicate disabled state */
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar Styling for Chat Messages */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
/* Scrollbar Styling for Chat Messages */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 8px; /* Width of scrollbar */
|
||||
}
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #cccccc; /* Gray scrollbar thumb */
|
||||
border-radius: 4px; /* Rounded scrollbar */
|
||||
}
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #aaaaaa; /* Darker gray on hover */
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================= */
|
||||
/* Minimize/Maximize Additions */
|
||||
/* ============================= */
|
||||
|
||||
/* Header Bar for Minimization */
|
||||
.chatbot-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin-bottom: 10px; /* Optional: slight spacing under the header */
|
||||
padding: 8px 10px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
/* Title in the header */
|
||||
.chatbot-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Minimize button in header */
|
||||
.minimize-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* When minimized, hide all but the header */
|
||||
.chatbot-container.minimized {
|
||||
height: auto;
|
||||
width: 350px;
|
||||
max-height: 50px; /* Just an example narrower width when minimized */
|
||||
padding: 0 0 10px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chatbot-container.minimized .chat-messages,
|
||||
.chatbot-container.minimized .chat-input-form {
|
||||
display: none; /* Hide chat body & input */
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import axios from "axios";
|
||||
import "./Chatbot.css";
|
||||
|
||||
const Chatbot = ({ context }) => {
|
||||
|
||||
const [messages, setMessages] = useState([
|
||||
{
|
||||
role: "assistant",
|
||||
@ -14,56 +13,27 @@ const Chatbot = ({ context }) => {
|
||||
const [input, setInput] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// NEW: track whether the chatbot is minimized or expanded
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
|
||||
const toggleMinimize = () => {
|
||||
setIsMinimized((prev) => !prev);
|
||||
};
|
||||
|
||||
const sendMessage = async (content) => {
|
||||
const userMessage = { role: "user", content };
|
||||
|
||||
// Ensure career data is injected into every API call
|
||||
// Build your context summary
|
||||
const contextSummary = `
|
||||
You are an advanced AI career advisor for AptivaAI.
|
||||
Your role is to not only provide career suggestions but to analyze them based on salary potential, job stability, education costs, and market trends.
|
||||
|
||||
Use the following user-specific data:
|
||||
- Career Suggestions: ${context.careerSuggestions?.map((c) => c.title).join(", ") || "No suggestions available."}
|
||||
- Selected Career: ${context.selectedCareer?.title || "None"}
|
||||
- Job Description: ${context.careerDetails?.jobDescription || "No job description available."}
|
||||
- Expected Tasks: ${context.careerDetails?.tasks?.join(", ") || "No tasks available."}
|
||||
- Salary Data:
|
||||
${context.salaryData?.length > 0
|
||||
? context.salaryData.map(sd => `${sd.percentile}: $${sd.regionalSalary || "N/A"} (Regional), $${sd.nationalSalary || "N/A"} (National)`).join(", ")
|
||||
: "Not available"}
|
||||
- Schools Available: ${
|
||||
context.schools?.length > 0
|
||||
? context.schools
|
||||
.map(school => `${school.INSTNM} (Distance: ${school.distance || "N/A"}, Tuition: $${school["In_state cost"] || "N/A"})`)
|
||||
.join("; ")
|
||||
: "No schools available."
|
||||
}
|
||||
|
||||
- Economic Projections: ${
|
||||
context.economicProjections && Object.keys(context.economicProjections).length > 0
|
||||
? `2022 Employment: ${context.economicProjections["2022 Employment"] || "N/A"}, ` +
|
||||
`2032 Employment: ${context.economicProjections["2032 Employment"] || "N/A"}, ` +
|
||||
`Total Change: ${context.economicProjections["Total Change"] || "N/A"}`
|
||||
: "Not available"
|
||||
}
|
||||
|
||||
- User State: ${context.userState || "Not provided"}
|
||||
- User Area: ${context.areaTitle || "Not provided"}
|
||||
- User Zipcode: ${context.userZipcode || "Not provided"}
|
||||
|
||||
**Your response should ALWAYS provide analysis, not just list careers.**
|
||||
Example responses:
|
||||
- "If you're looking for a high salary right away, X might be a great option, but it has slow growth."
|
||||
- "If you prefer job stability, Y is projected to grow in demand over the next 10 years."
|
||||
- "If work-life balance is a priority, avoid Z as it has high stress and irregular hours."
|
||||
|
||||
If the user asks about "the best career," do not assume a single best choice. Instead, explain trade-offs like:
|
||||
- "If you want high pay now, X is great, but it has limited upward growth."
|
||||
- "If you prefer stability, Y is a better long-term bet."
|
||||
`;
|
||||
Your role is to provide analysis based on user data:
|
||||
- ...
|
||||
(Continue with your existing context data as before)
|
||||
`;
|
||||
|
||||
// Combine with existing messages
|
||||
const messagesToSend = [
|
||||
{ role: "system", content: contextSummary }, // Inject AptivaAI data on every request
|
||||
{ role: "system", content: contextSummary },
|
||||
...messages,
|
||||
userMessage,
|
||||
];
|
||||
@ -86,13 +56,17 @@ const Chatbot = ({ context }) => {
|
||||
);
|
||||
|
||||
const botMessage = response.data.choices[0].message;
|
||||
// The returned message has {role: "assistant", content: "..."}
|
||||
setMessages([...messages, userMessage, botMessage]);
|
||||
} catch (error) {
|
||||
console.error("Chatbot Error:", error);
|
||||
setMessages([
|
||||
...messages,
|
||||
userMessage,
|
||||
{ role: "assistant", content: "Error: Unable to fetch response. Please try again." },
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Error: Unable to fetch response. Please try again.",
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -108,26 +82,46 @@ const Chatbot = ({ context }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chatbot-container">
|
||||
<div className={`chatbot-container ${isMinimized ? "minimized" : ""}`}>
|
||||
{/* Header Bar for Minimize/Maximize */}
|
||||
<div className="chatbot-header">
|
||||
<span className="chatbot-title">Career Chatbot</span>
|
||||
<button className="minimize-btn" onClick={toggleMinimize}>
|
||||
{isMinimized ? "▼" : "▲"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* If not minimized, show the chat messages and input */}
|
||||
{!isMinimized && (
|
||||
<>
|
||||
<div className="chat-messages">
|
||||
{messages.map((msg, index) => (
|
||||
<div key={index} className={`message ${msg.role}`}>
|
||||
{messages.map((msg, index) => {
|
||||
// default to 'bot' if role not user or assistant
|
||||
const roleClass =
|
||||
msg.role === "user" ? "user" : msg.role === "assistant" ? "bot" : "bot";
|
||||
return (
|
||||
<div key={index} className={`message ${roleClass}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
))}
|
||||
{loading && <div className="message assistant">Typing...</div>}
|
||||
);
|
||||
})}
|
||||
{loading && <div className="message bot typing">Typing...</div>}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="chat-input-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ask a question..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button type="submit" disabled={loading}>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -40,13 +40,9 @@ function Dashboard() {
|
||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
||||
const [sessionHandled, setSessionHandled] = useState(false);
|
||||
|
||||
const loadingSuggestions = loading;
|
||||
const popoutVisible = !!selectedCareer;
|
||||
|
||||
const handleUnauthorized = () => {
|
||||
if (!sessionHandled) {
|
||||
setSessionHandled(true);
|
||||
setShowSessionExpiredModal(true); // Show session expired modal
|
||||
}
|
||||
};
|
||||
// Function to handle the token check and fetch requests
|
||||
const authFetch = async (url, options = {}, onUnauthorized) => {
|
||||
const token = localStorage.getItem("token");
|
||||
@ -361,7 +357,8 @@ function Dashboard() {
|
||||
};
|
||||
|
||||
const renderLoadingOverlay = () => {
|
||||
if (!loading) return null;
|
||||
// If we are NOT loading suggestions, or if the popout is visible, hide the overlay
|
||||
if (!loadingSuggestions || popoutVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
||||
|
@ -1,53 +1,57 @@
|
||||
import React from "react";
|
||||
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 SchoolFilters from './SchoolFilters';
|
||||
import './PopoutPanel.css';
|
||||
import { useState, useEffect } from 'react';
|
||||
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', // Passed explicitly from Dashboard
|
||||
userState = "N/A",
|
||||
loading = false,
|
||||
error = null,
|
||||
closePanel,
|
||||
updateChatbotContext,
|
||||
}) {
|
||||
// Original local states
|
||||
const [isCalculated, setIsCalculated] = useState(false);
|
||||
const [results, setResults] = useState([]); // Store loan repayment calculation results
|
||||
const [results, setResults] = useState([]);
|
||||
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 token = localStorage.getItem('token');
|
||||
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',
|
||||
title = "Career Details",
|
||||
economicProjections = {},
|
||||
salaryData = [],
|
||||
schools = [],
|
||||
} = data || {};
|
||||
|
||||
// Clear results if sorting or filters change
|
||||
useEffect(() => {
|
||||
setResults([]);
|
||||
setIsCalculated(false);
|
||||
}, [sortBy, maxTuition, maxDistance]); // Ensure no other dependencies!
|
||||
}, [sortBy, maxTuition, maxDistance]);
|
||||
|
||||
// Derive program lengths from school CREDDESC
|
||||
useEffect(() => {
|
||||
setProgramLengths(schools.map(school => getProgramLength(school['CREDDESC'])));
|
||||
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,
|
||||
@ -55,102 +59,90 @@ function PopoutPanel({
|
||||
salaryData,
|
||||
economicProjections,
|
||||
results,
|
||||
persistedROI, // ✅ Make sure ROI is included!
|
||||
persistedROI,
|
||||
});
|
||||
} else {
|
||||
}
|
||||
}, [data, schools, salaryData, economicProjections, results, persistedROI, updateChatbotContext]);
|
||||
|
||||
}, [
|
||||
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">
|
||||
<button className="close-btn" onClick={closePanel}>X</button>
|
||||
<h2>Loading Career Details...</h2>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// Get program length for calculating tuition
|
||||
const getProgramLength = (degreeType) => {
|
||||
// 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("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
|
||||
return 4;
|
||||
}
|
||||
|
||||
// Original close logic
|
||||
function handleClosePanel() {
|
||||
setResults([]);
|
||||
setIsCalculated(false);
|
||||
closePanel();
|
||||
}
|
||||
|
||||
/** 🔹 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;
|
||||
});
|
||||
|
||||
const handlePlanMyPath = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
// Original PlanMyPath logic
|
||||
async function handlePlanMyPath() {
|
||||
if (!token) {
|
||||
alert("You need to be logged in to create a career path.");
|
||||
return;
|
||||
}
|
||||
|
||||
const careerName = title; // Get the career name from the `data` object
|
||||
|
||||
// First, check if a career path already exists for the selected career
|
||||
const checkResponse = await fetch('/api/premium/planned-path/latest', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const allPathsResponse = await fetch(`${process.env.REACT_APP_API_URL}/premium/planned-path/all`, {
|
||||
method: 'GET',
|
||||
const allPathsResponse = await fetch(
|
||||
`${process.env.REACT_APP_API_URL}/premium/planned-path/all`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
if (!allPathsResponse.ok) throw new Error(`HTTP error ${allPathsResponse.status}`);
|
||||
|
||||
const { careerPath } = await allPathsResponse.json();
|
||||
const match = careerPath.find(path => path.career_name === data.title);
|
||||
const match = careerPath.find((path) => path.career_name === data.title);
|
||||
|
||||
if (match) {
|
||||
const decision = window.confirm(
|
||||
`A career path for "${data.title}" already exists.\n\nClick OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
|
||||
`A career path for "${data.title}" already exists.\n\n` +
|
||||
`Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
|
||||
);
|
||||
|
||||
|
||||
if (decision) {
|
||||
navigate('/financial-profile', {
|
||||
state: { selectedCareer: { career_path_id: match.id, career_name: data.title } }
|
||||
navigate("/financial-profile", {
|
||||
state: {
|
||||
selectedCareer: { career_path_id: match.id, career_name: data.title },
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -158,217 +150,298 @@ function PopoutPanel({
|
||||
|
||||
const newCareerPath = {
|
||||
career_path_id: uuidv4(),
|
||||
career_name: data.title
|
||||
career_name: data.title,
|
||||
};
|
||||
|
||||
const newResponse = await fetch(`${process.env.REACT_APP_API_URL}/premium/planned-path`, {
|
||||
method: 'POST',
|
||||
const newResponse = await fetch(
|
||||
`${process.env.REACT_APP_API_URL}/premium/planned-path`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(newCareerPath),
|
||||
});
|
||||
|
||||
if (!newResponse.ok) throw new Error('Failed to create new career path.');
|
||||
|
||||
navigate('/financial-profile', {
|
||||
state: { selectedCareer: { career_path_id: newCareerPath.career_path_id, career_name: data.title } }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in Plan My Path:', error);
|
||||
}
|
||||
};
|
||||
);
|
||||
if (!newResponse.ok) throw new Error("Failed to create new career path.");
|
||||
|
||||
navigate("/milestone-tracker", {
|
||||
state: {
|
||||
selectedCareer: {
|
||||
career_path_id: newCareerPath.career_path_id,
|
||||
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">
|
||||
{/* Header with Close & Plan My Path Buttons */}
|
||||
<div className="panel-header">
|
||||
<button className="close-btn" onClick={closePanel}>X</button>
|
||||
<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="plan-path-btn"
|
||||
onClick={handlePlanMyPath} // 🔥 Use the already-defined, correct handler
|
||||
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>
|
||||
|
||||
<h2>{title}</h2>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
{/* Title */}
|
||||
<h2 className="text-xl font-semibold">{title}</h2>
|
||||
|
||||
{/* Job Description and Tasks */}
|
||||
<div className="job-description">
|
||||
<h3>Job Description</h3>
|
||||
<p>{jobDescription || 'No description available'}</p>
|
||||
{/* 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>
|
||||
|
||||
<div className="job-tasks">
|
||||
<h3>Expected Tasks</h3>
|
||||
{/* 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>
|
||||
{tasks.map((task, index) => (
|
||||
<li key={index}>{task}</li>
|
||||
<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>No tasks available for this career path.</p>
|
||||
<p className="text-sm text-gray-500">No tasks available.</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>
|
||||
<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>No economic projections available for this career path.</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
No economic projections available.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Salary Data Points */}
|
||||
<div className="salary-data">
|
||||
<h3>Salary Data</h3>
|
||||
{/* 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>
|
||||
<table className="w-full text-sm text-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Percentile</th>
|
||||
<th>Regional Salary</th>
|
||||
<th>US Salary</th>
|
||||
<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, 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>
|
||||
{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>Salary data is not available.</p>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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)}
|
||||
style={{ marginLeft: '5px', padding: '2px', width: '100px' }}
|
||||
>
|
||||
<option value="tuition">Tuition</option>
|
||||
<option value="distance">Distance</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<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}
|
||||
step={1000}
|
||||
min={0}
|
||||
max={100000}
|
||||
style={{ width: '90px', padding: '2px' }}
|
||||
onChange={(e) => setMaxTuition(Number(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Distance (max mi):
|
||||
<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}
|
||||
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 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="no-schools-message">No schools of higher education are available in your state for this career path.</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
No schools matching your filters.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loan Repayment Analysis */}
|
||||
<h3>Loan Repayment Analysis</h3>
|
||||
<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, 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],
|
||||
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} // ✅ Store ROI after calculation
|
||||
setPersistedROI={setPersistedROI}
|
||||
/>
|
||||
|
||||
</section>
|
||||
|
||||
{/* 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>
|
||||
<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 Monthly Payment (with extra): $
|
||||
{result.totalMonthlyPayment}
|
||||
</p>
|
||||
<p>Total Loan Cost: ${result.totalLoanCost}</p>
|
||||
<p className={`net-gain ${parseFloat(result.netGain) < 0 ? 'negative' : 'positive'}`}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user