Fixed Profile links, MultiScenarioView fixes.
This commit is contained in:
parent
569626d489
commit
edeef42f5a
@ -359,7 +359,7 @@ app.post('/api/premium/ai/next-steps', authenticatePremiumUser, async (req, res)
|
|||||||
|
|
||||||
const threeYearsFromNow = new Date(now);
|
const threeYearsFromNow = new Date(now);
|
||||||
threeYearsFromNow.setFullYear(threeYearsFromNow.getFullYear() + 3);
|
threeYearsFromNow.setFullYear(threeYearsFromNow.getFullYear() + 3);
|
||||||
const isoThreeYearsFromNow = threeYearsFromNow.toISOString().slice(0, 10);
|
const isoThreeYearsFromNow = threeYearsFromNow.toISOString().slice(0, 10).slice(0, 10);
|
||||||
|
|
||||||
// 4) Construct ChatGPT messages
|
// 4) Construct ChatGPT messages
|
||||||
const messages = [
|
const messages = [
|
||||||
@ -2146,7 +2146,7 @@ app.post(
|
|||||||
SET resume_optimizations_used = 0,
|
SET resume_optimizations_used = 0,
|
||||||
resume_limit_reset = ?
|
resume_limit_reset = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, [resetDate.toISOString(), id]);
|
`, [resetDate.toISOString().slice(0, 10), id]);
|
||||||
|
|
||||||
userProfile.resume_optimizations_used = 0;
|
userProfile.resume_optimizations_used = 0;
|
||||||
}
|
}
|
||||||
@ -2202,7 +2202,7 @@ app.post(
|
|||||||
res.json({
|
res.json({
|
||||||
optimizedResume,
|
optimizedResume,
|
||||||
remainingOptimizations,
|
remainingOptimizations,
|
||||||
resetDate: resetDate.toISOString()
|
resetDate: resetDate.toISOString().slice(0, 10)
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error optimizing resume:', err);
|
console.error('Error optimizing resume:', err);
|
||||||
@ -2246,7 +2246,7 @@ app.get('/api/premium/resume/remaining', authenticatePremiumUser, async (req, re
|
|||||||
SET resume_optimizations_used = 0,
|
SET resume_optimizations_used = 0,
|
||||||
resume_limit_reset = ?
|
resume_limit_reset = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, [resetDate.toISOString(), id]);
|
`, [resetDate.toISOString().slice(0, 10), id]);
|
||||||
|
|
||||||
userProfile.resume_optimizations_used = 0;
|
userProfile.resume_optimizations_used = 0;
|
||||||
}
|
}
|
||||||
|
19
src/App.js
19
src/App.js
@ -261,10 +261,27 @@ function App() {
|
|||||||
>
|
>
|
||||||
Profile
|
Profile
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* DROPDOWN MENU FOR PROFILE */}
|
||||||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-48 z-50">
|
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-48 z-50">
|
||||||
{/* Account Profile, Financial Profile links */}
|
{/* Account (Links to UserProfile.js) */}
|
||||||
|
<Link
|
||||||
|
to="/profile"
|
||||||
|
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Financial Profile (Links to FinancialProfileForm.js) */}
|
||||||
|
<Link
|
||||||
|
to="/financial-profile"
|
||||||
|
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
Financial Profile
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* LOGOUT + UPGRADE BUTTONS */}
|
{/* LOGOUT + UPGRADE BUTTONS */}
|
||||||
|
@ -117,8 +117,8 @@ const MilestoneAddModal = ({
|
|||||||
end_month: impact.end_month !== null
|
end_month: impact.end_month !== null
|
||||||
? parseInt(impact.end_month, 10)
|
? parseInt(impact.end_month, 10)
|
||||||
: null,
|
: null,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date()..toISOString().slice(0, 10),
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date()..toISOString().slice(0, 10)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,30 @@
|
|||||||
// src/components/MilestoneCopyWizard.js
|
// src/components/MilestoneCopyWizard.js
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button } from './ui/button.js';
|
||||||
|
import authFetch from '../utils/authFetch.js';
|
||||||
|
|
||||||
export default function MilestoneCopyWizard({ milestone, authFetch, onClose }) {
|
export default function MilestoneCopyWizard({ milestone, onClose }) {
|
||||||
const [scenarios, setScenarios] = useState([]);
|
const [scenarios, setScenarios] = useState([]);
|
||||||
const [selectedScenarios, setSelectedScenarios] = useState([]);
|
const [selectedScenarios, setSelectedScenarios] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// fetch /api/premium/career-profile/all => setScenarios
|
if (!milestone) return;
|
||||||
}, [authFetch]);
|
// 1) load all scenarios
|
||||||
|
async function loadAllScenarios() {
|
||||||
|
try {
|
||||||
|
const resp = await authFetch('/api/premium/career-profile/all');
|
||||||
|
if (!resp.ok) {
|
||||||
|
console.error('Failed to load all scenarios =>', resp.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
setScenarios(data.careerProfiles || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('MilestoneCopyWizard => error loading scenarios:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadAllScenarios();
|
||||||
|
}, [milestone]);
|
||||||
|
|
||||||
function toggleScenario(id) {
|
function toggleScenario(id) {
|
||||||
setSelectedScenarios((prev) =>
|
setSelectedScenarios((prev) =>
|
||||||
@ -16,29 +33,85 @@ export default function MilestoneCopyWizard({ milestone, authFetch, onClose }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCopy() {
|
async function handleCopy() {
|
||||||
// POST => /api/premium/milestone/copy
|
if (!milestone || !selectedScenarios.length) {
|
||||||
// with { milestoneId: milestone.id, scenarioIds: selectedScenarios }
|
onClose(false);
|
||||||
// Then onClose(true)
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 2) call your copy endpoint
|
||||||
|
const payload = {
|
||||||
|
milestoneId: milestone.id,
|
||||||
|
scenarioIds: selectedScenarios
|
||||||
|
};
|
||||||
|
const res = await authFetch('/api/premium/milestone/copy', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text();
|
||||||
|
alert(txt || 'Failed to copy milestone');
|
||||||
|
onClose(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClose(true); // success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error copying milestone =>', err);
|
||||||
|
alert('Error copying milestone');
|
||||||
|
onClose(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!milestone) return null;
|
if (!milestone) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="copy-wizard-backdrop">
|
<div
|
||||||
<div className="copy-wizard-content">
|
style={{
|
||||||
<h3>Copy: {milestone.title}</h3>
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
background: 'rgba(0,0,0,0.4)',
|
||||||
|
zIndex: 99999,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#fff',
|
||||||
|
width: '400px',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h4>Copy Milestone:</h4>
|
||||||
|
<p style={{ fontWeight: 'bold' }}>{milestone.title}</p>
|
||||||
|
|
||||||
|
<div style={{ maxHeight: '250px', overflowY: 'auto', border: '1px solid #ccc', padding: '0.5rem' }}>
|
||||||
|
{scenarios.length === 0 && <p>No scenarios found.</p>}
|
||||||
{scenarios.map((s) => (
|
{scenarios.map((s) => (
|
||||||
<label key={s.id}>
|
<div key={s.id} style={{ marginBottom: '0.25rem' }}>
|
||||||
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedScenarios.includes(s.id)}
|
checked={selectedScenarios.includes(s.id)}
|
||||||
onChange={() => toggleScenario(s.id)}
|
onChange={() => toggleScenario(s.id)}
|
||||||
/>
|
/>
|
||||||
{s.career_name}
|
{s.scenario_title || s.career_name || '(Untitled)'}
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
<br />
|
</div>
|
||||||
<button onClick={() => onClose(false)}>Cancel</button>
|
|
||||||
<button onClick={() => handleCopy()}>Copy</button>
|
<div style={{ marginTop: '1rem', textAlign: 'right' }}>
|
||||||
|
<Button onClick={() => onClose(false)} style={{ marginRight: '0.5rem' }}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCopy}>Copy</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -642,7 +642,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
programLength: collegeData.programLength,
|
programLength: collegeData.programLength,
|
||||||
expectedSalary: collegeData.expectedSalary,
|
expectedSalary: collegeData.expectedSalary,
|
||||||
|
|
||||||
startDate: new Date().toISOString(),
|
startDate: new Date().toISOString().slice(0, 10),
|
||||||
simulationYears,
|
simulationYears,
|
||||||
milestoneImpacts: allImpacts,
|
milestoneImpacts: allImpacts,
|
||||||
|
|
||||||
@ -703,7 +703,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
const [clickCount, setClickCount] = useState(() => {
|
const [clickCount, setClickCount] = useState(() => {
|
||||||
const storedCount = localStorage.getItem('aiClickCount');
|
const storedCount = localStorage.getItem('aiClickCount');
|
||||||
const storedDate = localStorage.getItem('aiClickDate');
|
const storedDate = localStorage.getItem('aiClickDate');
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10).slice(0, 10);
|
||||||
if (storedDate !== today) {
|
if (storedDate !== today) {
|
||||||
localStorage.setItem('aiClickDate', today);
|
localStorage.setItem('aiClickDate', today);
|
||||||
localStorage.setItem('aiClickCount', '0');
|
localStorage.setItem('aiClickCount', '0');
|
||||||
|
@ -7,31 +7,21 @@ import { Button } from './ui/button.js';
|
|||||||
export default function MultiScenarioView() {
|
export default function MultiScenarioView() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// The user’s single overall financial profile
|
|
||||||
const [financialProfile, setFinancialProfile] = useState(null);
|
const [financialProfile, setFinancialProfile] = useState(null);
|
||||||
|
|
||||||
// The list of scenario "headers" (rows from career_profiles)
|
|
||||||
const [scenarios, setScenarios] = useState([]);
|
const [scenarios, setScenarios] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadScenariosAndFinancial();
|
loadScenariosAndFinancial();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch user’s financial profile + scenario list
|
|
||||||
*/
|
|
||||||
async function loadScenariosAndFinancial() {
|
async function loadScenariosAndFinancial() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) fetch user’s global financialProfile
|
|
||||||
const finRes = await authFetch('/api/premium/financial-profile');
|
const finRes = await authFetch('/api/premium/financial-profile');
|
||||||
if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`);
|
if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`);
|
||||||
const finData = await finRes.json();
|
const finData = await finRes.json();
|
||||||
|
|
||||||
// 2) fetch scenario list
|
|
||||||
const scenRes = await authFetch('/api/premium/career-profile/all');
|
const scenRes = await authFetch('/api/premium/career-profile/all');
|
||||||
if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
|
if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
|
||||||
const scenData = await scenRes.json();
|
const scenData = await scenRes.json();
|
||||||
@ -39,58 +29,57 @@ export default function MultiScenarioView() {
|
|||||||
setFinancialProfile(finData);
|
setFinancialProfile(finData);
|
||||||
setScenarios(scenData.careerProfiles || []);
|
setScenarios(scenData.careerProfiles || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('MultiScenarioView load error:', err);
|
console.error('MultiScenarioView =>', err);
|
||||||
setError(err.message || 'Failed to load multi-scenarios');
|
setError(err.message || 'Failed to load');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a brand-new scenario with minimal defaults
|
|
||||||
*/
|
|
||||||
async function handleAddScenario() {
|
async function handleAddScenario() {
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
career_name: 'New Scenario ' + new Date().toLocaleDateString(),
|
career_name: 'New Scenario ' + new Date().toLocaleDateString(),
|
||||||
status: 'planned',
|
status: 'planned',
|
||||||
start_date: new Date().toISOString(),
|
// slice(0,10) to avoid timestamps
|
||||||
|
start_date: new Date().toISOString().slice(0, 10),
|
||||||
college_enrollment_status: 'not_enrolled',
|
college_enrollment_status: 'not_enrolled',
|
||||||
currently_working: 'no'
|
currently_working: 'no'
|
||||||
};
|
};
|
||||||
|
const r = await authFetch('/api/premium/career-profile', {
|
||||||
const res = await authFetch('/api/premium/career-profile', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
|
if (!r.ok) throw new Error(`Add scenario error => ${r.status}`);
|
||||||
|
|
||||||
// reload
|
|
||||||
await loadScenariosAndFinancial();
|
await loadScenariosAndFinancial();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone a scenario:
|
|
||||||
* (A) create new scenario row from old scenario fields
|
|
||||||
* (B) also clone old scenario’s college_profile
|
|
||||||
*/
|
|
||||||
async function handleCloneScenario(oldScenario) {
|
async function handleCloneScenario(oldScenario) {
|
||||||
try {
|
try {
|
||||||
// 1) create the new scenario row
|
// convert oldScenario.start_date to just YYYY-MM-DD
|
||||||
|
const cloneStart = oldScenario.start_date
|
||||||
|
? oldScenario.start_date.slice(0, 10)
|
||||||
|
: new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
const scenarioPayload = {
|
const scenarioPayload = {
|
||||||
scenario_title: oldScenario.scenario_title
|
scenario_title: oldScenario.scenario_title
|
||||||
? oldScenario.scenario_title + ' (Copy)'
|
? oldScenario.scenario_title + ' (Copy)'
|
||||||
: null,
|
: 'Untitled (Copy)',
|
||||||
career_name: oldScenario.career_name
|
career_name: oldScenario.career_name
|
||||||
? oldScenario.career_name + ' (Copy)'
|
? oldScenario.career_name + ' (Copy)'
|
||||||
: 'Untitled (Copy)',
|
: 'Unknown Career',
|
||||||
status: oldScenario.status,
|
status: oldScenario.status,
|
||||||
start_date: oldScenario.start_date,
|
// also do the slice if projected_end_date is set
|
||||||
projected_end_date: oldScenario.projected_end_date,
|
start_date: oldScenario.start_date
|
||||||
|
? oldScenario.start_date.slice(0, 10)
|
||||||
|
: '',
|
||||||
|
projected_end_date: oldScenario.projected_end_date
|
||||||
|
? oldScenario.projected_end_date.slice(0, 10)
|
||||||
|
: '',
|
||||||
college_enrollment_status: oldScenario.college_enrollment_status,
|
college_enrollment_status: oldScenario.college_enrollment_status,
|
||||||
currently_working: oldScenario.currently_working || 'no',
|
currently_working: oldScenario.currently_working || 'no',
|
||||||
|
|
||||||
@ -106,107 +95,195 @@ export default function MultiScenarioView() {
|
|||||||
planned_additional_income: oldScenario.planned_additional_income
|
planned_additional_income: oldScenario.planned_additional_income
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await authFetch('/api/premium/career-profile', {
|
const createRes = await authFetch('/api/premium/career-profile', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(scenarioPayload)
|
body: JSON.stringify(scenarioPayload)
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`);
|
if (!createRes.ok) {
|
||||||
|
throw new Error(`Clone scenario error: ${createRes.status}`);
|
||||||
// parse the newly created scenario_id
|
}
|
||||||
const newScenarioData = await res.json();
|
const newScenarioData = await createRes.json();
|
||||||
const newScenarioId = newScenarioData.career_profile_id;
|
const newScenarioId = newScenarioData.career_profile_id;
|
||||||
|
|
||||||
// 2) Clone the old scenario’s college_profile => new scenario
|
// clone college
|
||||||
await cloneCollegeProfile(oldScenario.id, newScenarioId);
|
await cloneCollegeProfile(oldScenario.id, newScenarioId);
|
||||||
|
|
||||||
// 3) reload
|
// clone milestones
|
||||||
|
await cloneAllMilestones(oldScenario.id, newScenarioId);
|
||||||
|
|
||||||
await loadScenariosAndFinancial();
|
await loadScenariosAndFinancial();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Clone scenario failed: ${err.message}`);
|
alert('Failed to clone scenario => ' + err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cloneCollegeProfile(oldScenarioId, newScenarioId) {
|
async function cloneCollegeProfile(oldId, newId) {
|
||||||
try {
|
try {
|
||||||
// fetch old scenario’s college_profile
|
const cRes = await authFetch(`/api/premium/college-profile?careerProfileId=${oldId}`);
|
||||||
const getRes = await authFetch(
|
if (!cRes.ok) return;
|
||||||
`/api/premium/college-profile?careerProfileId=${oldScenarioId}`
|
let oldC = await cRes.json();
|
||||||
);
|
if (Array.isArray(oldC)) oldC = oldC[0] || null;
|
||||||
if (!getRes.ok) {
|
if (!oldC || !oldC.id) return;
|
||||||
console.warn(
|
|
||||||
'Could not fetch old college profile for scenarioId=' + oldScenarioId
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldCollegeData = await getRes.json();
|
// you can do date-slice on expected_graduation if needed
|
||||||
if (Array.isArray(oldCollegeData)) {
|
const pay = {
|
||||||
oldCollegeData = oldCollegeData[0] || null;
|
career_profile_id: newId,
|
||||||
}
|
selected_school: oldC.selected_school,
|
||||||
|
selected_program: oldC.selected_program,
|
||||||
if (!oldCollegeData || !oldCollegeData.id) {
|
program_type: oldC.program_type,
|
||||||
// no old college profile => nothing to clone
|
academic_calendar: oldC.academic_calendar,
|
||||||
return;
|
is_in_state: oldC.is_in_state,
|
||||||
}
|
is_in_district: oldC.is_in_district,
|
||||||
|
is_online: oldC.is_online,
|
||||||
// build new payload
|
college_enrollment_status: oldC.college_enrollment_status,
|
||||||
const clonePayload = {
|
annual_financial_aid: oldC.annual_financial_aid,
|
||||||
career_profile_id: newScenarioId,
|
existing_college_debt: oldC.existing_college_debt,
|
||||||
|
tuition_paid: oldC.tuition_paid,
|
||||||
selected_school: oldCollegeData.selected_school,
|
tuition: oldC.tuition,
|
||||||
selected_program: oldCollegeData.selected_program,
|
loan_deferral_until_graduation: oldC.loan_deferral_until_graduation,
|
||||||
program_type: oldCollegeData.program_type,
|
loan_term: oldC.loan_term,
|
||||||
academic_calendar: oldCollegeData.academic_calendar,
|
interest_rate: oldC.interest_rate,
|
||||||
|
extra_payment: oldC.extra_payment,
|
||||||
is_in_state: oldCollegeData.is_in_state,
|
credit_hours_per_year: oldC.credit_hours_per_year,
|
||||||
is_in_district: oldCollegeData.is_in_district,
|
hours_completed: oldC.hours_completed,
|
||||||
is_online: oldCollegeData.is_online,
|
program_length: oldC.program_length,
|
||||||
college_enrollment_status: oldCollegeData.college_enrollment_status,
|
credit_hours_required: oldC.credit_hours_required,
|
||||||
|
expected_graduation: oldC.expected_graduation
|
||||||
annual_financial_aid: oldCollegeData.annual_financial_aid,
|
? oldC.expected_graduation.slice(0, 10)
|
||||||
existing_college_debt: oldCollegeData.existing_college_debt,
|
: '',
|
||||||
tuition_paid: oldCollegeData.tuition_paid,
|
expected_salary: oldC.expected_salary
|
||||||
tuition: oldCollegeData.tuition,
|
|
||||||
loan_deferral_until_graduation: oldCollegeData.loan_deferral_until_graduation,
|
|
||||||
loan_term: oldCollegeData.loan_term,
|
|
||||||
interest_rate: oldCollegeData.interest_rate,
|
|
||||||
extra_payment: oldCollegeData.extra_payment,
|
|
||||||
|
|
||||||
credit_hours_per_year: oldCollegeData.credit_hours_per_year,
|
|
||||||
hours_completed: oldCollegeData.hours_completed,
|
|
||||||
program_length: oldCollegeData.program_length,
|
|
||||||
credit_hours_required: oldCollegeData.credit_hours_required,
|
|
||||||
expected_graduation: oldCollegeData.expected_graduation,
|
|
||||||
expected_salary: oldCollegeData.expected_salary
|
|
||||||
};
|
};
|
||||||
|
const pRes = await authFetch('/api/premium/college-profile', {
|
||||||
// insert new row in college_profiles
|
|
||||||
const postRes = await authFetch('/api/premium/college-profile', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(clonePayload)
|
body: JSON.stringify(pay)
|
||||||
});
|
});
|
||||||
if (!postRes.ok) {
|
if (!pRes.ok) {
|
||||||
console.warn(
|
console.warn('Clone college failed =>', pRes.status);
|
||||||
'Could not clone old collegeProfile => new scenario',
|
|
||||||
postRes.status
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error cloning college profile:', err);
|
console.error('cloneCollegeProfile =>', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneAllMilestones(oldId, newId) {
|
||||||
|
try {
|
||||||
|
const mRes = await authFetch(
|
||||||
|
`/api/premium/milestones?careerProfileId=${oldId}`
|
||||||
|
);
|
||||||
|
if (!mRes.ok) {
|
||||||
|
console.warn('No old milestones => skip');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = await mRes.json();
|
||||||
|
const oldList = d.milestones || [];
|
||||||
|
for (const m of oldList) {
|
||||||
|
// create new milestone
|
||||||
|
const newMileId = await cloneSingleMilestone(m, newId);
|
||||||
|
// tasks
|
||||||
|
await cloneTasks(m.id, newMileId);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('cloneAllMilestones =>', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function cloneSingleMilestone(oldM, newScenarioId) {
|
||||||
|
try {
|
||||||
|
// remove timestamps from oldM.date
|
||||||
|
const justDate = oldM.date ? oldM.date.slice(0, 10) : '';
|
||||||
|
const pay = {
|
||||||
|
title: oldM.title,
|
||||||
|
description: oldM.description,
|
||||||
|
date: justDate,
|
||||||
|
career_profile_id: newScenarioId,
|
||||||
|
progress: oldM.progress,
|
||||||
|
status: oldM.status,
|
||||||
|
is_universal: oldM.is_universal
|
||||||
|
};
|
||||||
|
const r = await authFetch('/api/premium/milestone', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(pay)
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
console.warn('cloneSingleMilestone =>', r.status);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const j = await r.json();
|
||||||
|
let mid = null;
|
||||||
|
if (Array.isArray(j)) {
|
||||||
|
mid = j[0]?.id || null;
|
||||||
|
} else if (j?.id) {
|
||||||
|
mid = j.id;
|
||||||
|
}
|
||||||
|
// impacts
|
||||||
|
if (mid) {
|
||||||
|
await cloneMilestoneImpacts(oldM.id, mid);
|
||||||
|
}
|
||||||
|
return mid;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('cloneSingleMilestone =>', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function cloneMilestoneImpacts(oldMId, newMId) {
|
||||||
|
try {
|
||||||
|
const iRes = await authFetch(`/api/premium/milestone-impacts?milestone_id=${oldMId}`);
|
||||||
|
if (!iRes.ok) return;
|
||||||
|
const d = await iRes.json();
|
||||||
|
const arr = d.impacts || [];
|
||||||
|
for (const imp of arr) {
|
||||||
|
const justStart = imp.start_date ? imp.start_date.slice(0, 10) : null;
|
||||||
|
const justEnd = imp.end_date ? imp.end_date.slice(0, 10) : null;
|
||||||
|
const pay = {
|
||||||
|
milestone_id: newMId,
|
||||||
|
impact_type: imp.impact_type,
|
||||||
|
direction: imp.direction,
|
||||||
|
amount: imp.amount,
|
||||||
|
start_date: justStart,
|
||||||
|
end_date: justEnd
|
||||||
|
};
|
||||||
|
await authFetch('/api/premium/milestone-impacts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(pay)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('cloneMilestoneImpacts =>', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function cloneTasks(oldMId, newMId) {
|
||||||
|
try {
|
||||||
|
const tRes = await authFetch(`/api/premium/tasks?milestone_id=${oldMId}`);
|
||||||
|
if (!tRes.ok) return;
|
||||||
|
const d = await tRes.json();
|
||||||
|
const arr = d.tasks || [];
|
||||||
|
for (const tk of arr) {
|
||||||
|
const pay = {
|
||||||
|
milestone_id: newMId,
|
||||||
|
title: tk.title,
|
||||||
|
description: tk.description,
|
||||||
|
due_date: tk.due_date ? tk.due_date.slice(0, 10) : ''
|
||||||
|
};
|
||||||
|
await authFetch('/api/premium/tasks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(pay)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('cloneTasks =>', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveScenario(id) {
|
async function handleRemoveScenario(id) {
|
||||||
const confirmDel = window.confirm('Delete this scenario?');
|
const c = window.confirm('Delete scenario?');
|
||||||
if (!confirmDel) return;
|
if (!c) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/premium/career-profile/${id}`, {
|
const r = await authFetch(`/api/premium/career-profile/${id}`, { method: 'DELETE' });
|
||||||
method: 'DELETE'
|
if (!r.ok) throw new Error(`Delete scenario => ${r.status}`);
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`);
|
|
||||||
await loadScenariosAndFinancial();
|
await loadScenariosAndFinancial();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
@ -216,30 +293,22 @@ export default function MultiScenarioView() {
|
|||||||
if (loading) return <p>Loading scenarios...</p>;
|
if (loading) return <p>Loading scenarios...</p>;
|
||||||
if (error) return <p style={{ color: 'red' }}>{error}</p>;
|
if (error) return <p style={{ color: 'red' }}>{error}</p>;
|
||||||
|
|
||||||
// show only first 2 scenarios
|
const visible = scenarios.slice(0, 2);
|
||||||
const visibleScenarios = scenarios.slice(0, 2);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ margin: '1rem' }}>
|
<div style={{ margin: '1rem' }}>
|
||||||
{/* Add Scenario button */}
|
<Button onClick={handleAddScenario} style={{ marginBottom: '1rem' }}>
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
+ Add Scenario
|
||||||
<Button onClick={handleAddScenario}>+ Add Scenario</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Display 1 or 2 scenarios side by side */}
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
{visibleScenarios.map((sc) => (
|
{visible.map(sc => (
|
||||||
<ScenarioContainer
|
<ScenarioContainer
|
||||||
key={sc.id}
|
key={sc.id}
|
||||||
scenario={sc}
|
scenario={sc}
|
||||||
financialProfile={financialProfile}
|
financialProfile={financialProfile}
|
||||||
onClone={handleCloneScenario} // <--- pass down
|
onClone={handleCloneScenario}
|
||||||
onRemove={handleRemoveScenario} // <--- pass down
|
onRemove={handleRemoveScenario}
|
||||||
onEdit={(sc) => {
|
|
||||||
console.log('Edit scenario clicked:', sc);
|
|
||||||
// or open a modal if you prefer
|
|
||||||
}}
|
|
||||||
hideMilestones // if you want to hide milestone details
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,7 +56,7 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
inCollege: isInCollege,
|
inCollege: isInCollege,
|
||||||
// fallback defaults, or use user-provided
|
// fallback defaults, or use user-provided
|
||||||
status: prevData.status || 'planned',
|
status: prevData.status || 'planned',
|
||||||
start_date: prevData.start_date || new Date().toISOString().slice(0, 10),
|
start_date: prevData.start_date || new Date().toISOString().slice(0, 10).slice(0, 10),
|
||||||
projected_end_date: prevData.projected_end_date || null
|
projected_end_date: prevData.projected_end_date || null
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -424,7 +424,7 @@ export default function ScenarioEditModal({
|
|||||||
expectedSalary:
|
expectedSalary:
|
||||||
collegeRow.expected_salary || financialData.current_salary || 0,
|
collegeRow.expected_salary || financialData.current_salary || 0,
|
||||||
|
|
||||||
startDate: scenarioRow.start_date || new Date().toISOString(),
|
startDate: scenarioRow.start_date || new Date().toISOString().slice(0, 10),
|
||||||
simulationYears: 20,
|
simulationYears: 20,
|
||||||
milestoneImpacts: []
|
milestoneImpacts: []
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user