Fixed Profile links, MultiScenarioView fixes.

This commit is contained in:
Josh 2025-05-30 14:35:29 +00:00
parent 569626d489
commit edeef42f5a
9 changed files with 761 additions and 381 deletions

View File

@ -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;
}

View File

@ -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 */}

View File

@ -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)
})
});
}

View File

@ -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>
);

View File

@ -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');

View File

@ -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 users 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 users financial profile + scenario list
*/
async function loadScenariosAndFinancial() {
setLoading(true);
setError(null);
try {
// 1) fetch users 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 scenarios 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 scenarios 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 scenarios 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>

View File

@ -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

View File

@ -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: []
};