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);
|
||||
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
|
||||
const messages = [
|
||||
@ -2146,7 +2146,7 @@ app.post(
|
||||
SET resume_optimizations_used = 0,
|
||||
resume_limit_reset = ?
|
||||
WHERE id = ?
|
||||
`, [resetDate.toISOString(), id]);
|
||||
`, [resetDate.toISOString().slice(0, 10), id]);
|
||||
|
||||
userProfile.resume_optimizations_used = 0;
|
||||
}
|
||||
@ -2202,7 +2202,7 @@ app.post(
|
||||
res.json({
|
||||
optimizedResume,
|
||||
remainingOptimizations,
|
||||
resetDate: resetDate.toISOString()
|
||||
resetDate: resetDate.toISOString().slice(0, 10)
|
||||
});
|
||||
} catch (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,
|
||||
resume_limit_reset = ?
|
||||
WHERE id = ?
|
||||
`, [resetDate.toISOString(), id]);
|
||||
`, [resetDate.toISOString().slice(0, 10), id]);
|
||||
|
||||
userProfile.resume_optimizations_used = 0;
|
||||
}
|
||||
|
59
src/App.js
59
src/App.js
@ -243,28 +243,45 @@ function App() {
|
||||
</div>
|
||||
|
||||
{/* 5) Profile */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
style={{ color: '#1f2937' }}
|
||||
className={`
|
||||
bg-white
|
||||
border border-gray-300
|
||||
hover:bg-gray-100
|
||||
hover:text-blue-700
|
||||
whitespace-nowrap
|
||||
text-xs sm:text-sm md:text-base
|
||||
font-semibold
|
||||
min-w-0
|
||||
max-w-[90px]
|
||||
truncate
|
||||
`}
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
<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 */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
style={{ color: '#1f2937' }}
|
||||
className={`
|
||||
bg-white
|
||||
border border-gray-300
|
||||
hover:bg-gray-100
|
||||
hover:text-blue-700
|
||||
whitespace-nowrap
|
||||
text-xs sm:text-sm md:text-base
|
||||
font-semibold
|
||||
min-w-0
|
||||
max-w-[90px]
|
||||
truncate
|
||||
`}
|
||||
>
|
||||
Profile
|
||||
</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">
|
||||
{/* 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>
|
||||
|
||||
</nav>
|
||||
|
||||
{/* LOGOUT + UPGRADE BUTTONS */}
|
||||
|
@ -117,8 +117,8 @@ const MilestoneAddModal = ({
|
||||
end_month: impact.end_month !== null
|
||||
? parseInt(impact.end_month, 10)
|
||||
: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
created_at: new Date()..toISOString().slice(0, 10),
|
||||
updated_at: new Date()..toISOString().slice(0, 10)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
@ -1,13 +1,30 @@
|
||||
// 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 [selectedScenarios, setSelectedScenarios] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// fetch /api/premium/career-profile/all => setScenarios
|
||||
}, [authFetch]);
|
||||
if (!milestone) return;
|
||||
// 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) {
|
||||
setSelectedScenarios((prev) =>
|
||||
@ -16,29 +33,85 @@ export default function MilestoneCopyWizard({ milestone, authFetch, onClose }) {
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
// POST => /api/premium/milestone/copy
|
||||
// with { milestoneId: milestone.id, scenarioIds: selectedScenarios }
|
||||
// Then onClose(true)
|
||||
if (!milestone || !selectedScenarios.length) {
|
||||
onClose(false);
|
||||
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;
|
||||
|
||||
return (
|
||||
<div className="copy-wizard-backdrop">
|
||||
<div className="copy-wizard-content">
|
||||
<h3>Copy: {milestone.title}</h3>
|
||||
{scenarios.map((s) => (
|
||||
<label key={s.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedScenarios.includes(s.id)}
|
||||
onChange={() => toggleScenario(s.id)}
|
||||
/>
|
||||
{s.career_name}
|
||||
</label>
|
||||
))}
|
||||
<br />
|
||||
<button onClick={() => onClose(false)}>Cancel</button>
|
||||
<button onClick={() => handleCopy()}>Copy</button>
|
||||
<div
|
||||
style={{
|
||||
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) => (
|
||||
<div key={s.id} style={{ marginBottom: '0.25rem' }}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedScenarios.includes(s.id)}
|
||||
onChange={() => toggleScenario(s.id)}
|
||||
/>
|
||||
{s.scenario_title || s.career_name || '(Untitled)'}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1rem', textAlign: 'right' }}>
|
||||
<Button onClick={() => onClose(false)} style={{ marginRight: '0.5rem' }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCopy}>Copy</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -642,7 +642,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
||||
programLength: collegeData.programLength,
|
||||
expectedSalary: collegeData.expectedSalary,
|
||||
|
||||
startDate: new Date().toISOString(),
|
||||
startDate: new Date().toISOString().slice(0, 10),
|
||||
simulationYears,
|
||||
milestoneImpacts: allImpacts,
|
||||
|
||||
@ -703,7 +703,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
||||
const [clickCount, setClickCount] = useState(() => {
|
||||
const storedCount = localStorage.getItem('aiClickCount');
|
||||
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) {
|
||||
localStorage.setItem('aiClickDate', today);
|
||||
localStorage.setItem('aiClickCount', '0');
|
||||
|
@ -7,31 +7,21 @@ import { Button } from './ui/button.js';
|
||||
export default function MultiScenarioView() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// The user’s single overall financial profile
|
||||
const [financialProfile, setFinancialProfile] = useState(null);
|
||||
|
||||
// The list of scenario "headers" (rows from career_profiles)
|
||||
const [scenarios, setScenarios] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadScenariosAndFinancial();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Fetch user’s financial profile + scenario list
|
||||
*/
|
||||
async function loadScenariosAndFinancial() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// 1) fetch user’s global financialProfile
|
||||
const finRes = await authFetch('/api/premium/financial-profile');
|
||||
if (!finRes.ok) throw new Error(`FinancialProfile error: ${finRes.status}`);
|
||||
const finData = await finRes.json();
|
||||
|
||||
// 2) fetch scenario list
|
||||
const scenRes = await authFetch('/api/premium/career-profile/all');
|
||||
if (!scenRes.ok) throw new Error(`Scenarios error: ${scenRes.status}`);
|
||||
const scenData = await scenRes.json();
|
||||
@ -39,58 +29,57 @@ export default function MultiScenarioView() {
|
||||
setFinancialProfile(finData);
|
||||
setScenarios(scenData.careerProfiles || []);
|
||||
} catch (err) {
|
||||
console.error('MultiScenarioView load error:', err);
|
||||
setError(err.message || 'Failed to load multi-scenarios');
|
||||
console.error('MultiScenarioView =>', err);
|
||||
setError(err.message || 'Failed to load');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a brand-new scenario with minimal defaults
|
||||
*/
|
||||
async function handleAddScenario() {
|
||||
try {
|
||||
const body = {
|
||||
career_name: 'New Scenario ' + new Date().toLocaleDateString(),
|
||||
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',
|
||||
currently_working: 'no'
|
||||
};
|
||||
|
||||
const res = await authFetch('/api/premium/career-profile', {
|
||||
const r = await authFetch('/api/premium/career-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!res.ok) throw new Error(`Add scenario error: ${res.status}`);
|
||||
|
||||
// reload
|
||||
if (!r.ok) throw new Error(`Add scenario error => ${r.status}`);
|
||||
await loadScenariosAndFinancial();
|
||||
} catch (err) {
|
||||
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) {
|
||||
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 = {
|
||||
scenario_title: oldScenario.scenario_title
|
||||
? oldScenario.scenario_title + ' (Copy)'
|
||||
: null,
|
||||
: 'Untitled (Copy)',
|
||||
career_name: oldScenario.career_name
|
||||
? oldScenario.career_name + ' (Copy)'
|
||||
: 'Untitled (Copy)',
|
||||
: 'Unknown Career',
|
||||
status: oldScenario.status,
|
||||
start_date: oldScenario.start_date,
|
||||
projected_end_date: oldScenario.projected_end_date,
|
||||
// also do the slice if projected_end_date is set
|
||||
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,
|
||||
currently_working: oldScenario.currently_working || 'no',
|
||||
|
||||
@ -106,107 +95,195 @@ export default function MultiScenarioView() {
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(scenarioPayload)
|
||||
});
|
||||
if (!res.ok) throw new Error(`Clone scenario error: ${res.status}`);
|
||||
|
||||
// parse the newly created scenario_id
|
||||
const newScenarioData = await res.json();
|
||||
if (!createRes.ok) {
|
||||
throw new Error(`Clone scenario error: ${createRes.status}`);
|
||||
}
|
||||
const newScenarioData = await createRes.json();
|
||||
const newScenarioId = newScenarioData.career_profile_id;
|
||||
|
||||
// 2) Clone the old scenario’s college_profile => new scenario
|
||||
// clone college
|
||||
await cloneCollegeProfile(oldScenario.id, newScenarioId);
|
||||
|
||||
// 3) reload
|
||||
// clone milestones
|
||||
await cloneAllMilestones(oldScenario.id, newScenarioId);
|
||||
|
||||
await loadScenariosAndFinancial();
|
||||
} 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 {
|
||||
// fetch old scenario’s college_profile
|
||||
const getRes = await authFetch(
|
||||
`/api/premium/college-profile?careerProfileId=${oldScenarioId}`
|
||||
);
|
||||
if (!getRes.ok) {
|
||||
console.warn(
|
||||
'Could not fetch old college profile for scenarioId=' + oldScenarioId
|
||||
);
|
||||
return;
|
||||
}
|
||||
const cRes = await authFetch(`/api/premium/college-profile?careerProfileId=${oldId}`);
|
||||
if (!cRes.ok) return;
|
||||
let oldC = await cRes.json();
|
||||
if (Array.isArray(oldC)) oldC = oldC[0] || null;
|
||||
if (!oldC || !oldC.id) return;
|
||||
|
||||
let oldCollegeData = await getRes.json();
|
||||
if (Array.isArray(oldCollegeData)) {
|
||||
oldCollegeData = oldCollegeData[0] || null;
|
||||
}
|
||||
|
||||
if (!oldCollegeData || !oldCollegeData.id) {
|
||||
// no old college profile => nothing to clone
|
||||
return;
|
||||
}
|
||||
|
||||
// build new payload
|
||||
const clonePayload = {
|
||||
career_profile_id: newScenarioId,
|
||||
|
||||
selected_school: oldCollegeData.selected_school,
|
||||
selected_program: oldCollegeData.selected_program,
|
||||
program_type: oldCollegeData.program_type,
|
||||
academic_calendar: oldCollegeData.academic_calendar,
|
||||
|
||||
is_in_state: oldCollegeData.is_in_state,
|
||||
is_in_district: oldCollegeData.is_in_district,
|
||||
is_online: oldCollegeData.is_online,
|
||||
college_enrollment_status: oldCollegeData.college_enrollment_status,
|
||||
|
||||
annual_financial_aid: oldCollegeData.annual_financial_aid,
|
||||
existing_college_debt: oldCollegeData.existing_college_debt,
|
||||
tuition_paid: oldCollegeData.tuition_paid,
|
||||
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
|
||||
// you can do date-slice on expected_graduation if needed
|
||||
const pay = {
|
||||
career_profile_id: newId,
|
||||
selected_school: oldC.selected_school,
|
||||
selected_program: oldC.selected_program,
|
||||
program_type: oldC.program_type,
|
||||
academic_calendar: oldC.academic_calendar,
|
||||
is_in_state: oldC.is_in_state,
|
||||
is_in_district: oldC.is_in_district,
|
||||
is_online: oldC.is_online,
|
||||
college_enrollment_status: oldC.college_enrollment_status,
|
||||
annual_financial_aid: oldC.annual_financial_aid,
|
||||
existing_college_debt: oldC.existing_college_debt,
|
||||
tuition_paid: oldC.tuition_paid,
|
||||
tuition: oldC.tuition,
|
||||
loan_deferral_until_graduation: oldC.loan_deferral_until_graduation,
|
||||
loan_term: oldC.loan_term,
|
||||
interest_rate: oldC.interest_rate,
|
||||
extra_payment: oldC.extra_payment,
|
||||
credit_hours_per_year: oldC.credit_hours_per_year,
|
||||
hours_completed: oldC.hours_completed,
|
||||
program_length: oldC.program_length,
|
||||
credit_hours_required: oldC.credit_hours_required,
|
||||
expected_graduation: oldC.expected_graduation
|
||||
? oldC.expected_graduation.slice(0, 10)
|
||||
: '',
|
||||
expected_salary: oldC.expected_salary
|
||||
};
|
||||
|
||||
// insert new row in college_profiles
|
||||
const postRes = await authFetch('/api/premium/college-profile', {
|
||||
const pRes = await authFetch('/api/premium/college-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(clonePayload)
|
||||
body: JSON.stringify(pay)
|
||||
});
|
||||
if (!postRes.ok) {
|
||||
console.warn(
|
||||
'Could not clone old collegeProfile => new scenario',
|
||||
postRes.status
|
||||
);
|
||||
if (!pRes.ok) {
|
||||
console.warn('Clone college failed =>', pRes.status);
|
||||
}
|
||||
} 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) {
|
||||
const confirmDel = window.confirm('Delete this scenario?');
|
||||
if (!confirmDel) return;
|
||||
|
||||
const c = window.confirm('Delete scenario?');
|
||||
if (!c) return;
|
||||
try {
|
||||
const res = await authFetch(`/api/premium/career-profile/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!res.ok) throw new Error(`Delete scenario error: ${res.status}`);
|
||||
const r = await authFetch(`/api/premium/career-profile/${id}`, { method: 'DELETE' });
|
||||
if (!r.ok) throw new Error(`Delete scenario => ${r.status}`);
|
||||
await loadScenariosAndFinancial();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
@ -216,30 +293,22 @@ export default function MultiScenarioView() {
|
||||
if (loading) return <p>Loading scenarios...</p>;
|
||||
if (error) return <p style={{ color: 'red' }}>{error}</p>;
|
||||
|
||||
// show only first 2 scenarios
|
||||
const visibleScenarios = scenarios.slice(0, 2);
|
||||
const visible = scenarios.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div style={{ margin: '1rem' }}>
|
||||
{/* Add Scenario button */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<Button onClick={handleAddScenario}>+ Add Scenario</Button>
|
||||
</div>
|
||||
<Button onClick={handleAddScenario} style={{ marginBottom: '1rem' }}>
|
||||
+ Add Scenario
|
||||
</Button>
|
||||
|
||||
{/* Display 1 or 2 scenarios side by side */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
{visibleScenarios.map((sc) => (
|
||||
{visible.map(sc => (
|
||||
<ScenarioContainer
|
||||
key={sc.id}
|
||||
scenario={sc}
|
||||
financialProfile={financialProfile}
|
||||
onClone={handleCloneScenario} // <--- pass down
|
||||
onRemove={handleRemoveScenario} // <--- pass down
|
||||
onEdit={(sc) => {
|
||||
console.log('Edit scenario clicked:', sc);
|
||||
// or open a modal if you prefer
|
||||
}}
|
||||
hideMilestones // if you want to hide milestone details
|
||||
onClone={handleCloneScenario}
|
||||
onRemove={handleRemoveScenario}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -56,7 +56,7 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||
inCollege: isInCollege,
|
||||
// fallback defaults, or use user-provided
|
||||
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
|
||||
}));
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -424,7 +424,7 @@ export default function ScenarioEditModal({
|
||||
expectedSalary:
|
||||
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,
|
||||
milestoneImpacts: []
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user