From 5ad377b50edf6bdfec4c396dcfa4d9aa84a15fd5 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 18 Jul 2025 17:01:32 +0000 Subject: [PATCH] Fixed MilestoneEditModal and FinancialProjectionService impact signs. --- src/components/CareerCoach.js | 5 +- src/components/CareerRoadmap.js | 14 +- src/components/MilestoneAddModal.js | 344 ++++---- src/components/MilestoneEditModal.js | 1036 ++++++++++------------- src/utils/FinancialProjectionService.js | 16 +- user_profile.db | Bin 208896 -> 208896 bytes 6 files changed, 664 insertions(+), 751 deletions(-) diff --git a/src/components/CareerCoach.js b/src/components/CareerCoach.js index 84e1d58..8a6238a 100644 --- a/src/components/CareerCoach.js +++ b/src/components/CareerCoach.js @@ -240,8 +240,9 @@ I'm here to support you with personalized coaching. What would you like to focus setMessages((prev) => [...prev, { role: "assistant", content: friendlyReply }]); if (riskData && onAiRiskFetched) onAiRiskFetched(riskData); - if (createdMilestones.length && onMilestonesCreated) - onMilestonesCreated(createdMilestones.length); + if (createdMilestones.length && typeof onMilestonesCreated === 'function') { + onMilestonesCreated(); // no arg needed – just refetch + } } catch (err) { console.error(err); setMessages((prev) => [...prev, { role: "assistant", content: "Sorry, something went wrong." }]); diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js index 8924e41..0868ce8 100644 --- a/src/components/CareerRoadmap.js +++ b/src/components/CareerRoadmap.js @@ -37,6 +37,7 @@ import parseAIJson from "../utils/parseAIJson.js"; // your shared parser import InfoTooltip from "./ui/infoTooltip.js"; import differenceInMonths from 'date-fns/differenceInMonths'; + import "../styles/legacy/MilestoneTimeline.legacy.css"; // -------------- @@ -1295,6 +1296,14 @@ const fetchMilestones = useCallback(async () => { } // single rebuild }, [financialProfile, scenarioRow, careerProfileId]); // ← NOTICE: no buildProjection here +const handleMilestonesCreated = useCallback( + (count = 0) => { + // optional toast + if (count) console.log(`πŸ’Ύ ${count} milestone(s) saved – refreshing list…`); + fetchMilestones(); + }, + [fetchMilestones] +); return (
@@ -1524,7 +1533,10 @@ const fetchMilestones = useCallback(async () => { {/* Milestones – stacked list under chart */}
-

Milestones

+

+ Milestones + +

{ - // Basic milestone fields - const [title, setTitle] = useState(''); + scenarioId, // active scenario UUID + editMilestone = null // pass full row when editing +}) { + /* ────────────── state ────────────── */ + const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); + const [impacts, setImpacts] = useState([]); - // We'll store an array of impacts. Each impact is { impact_type, direction, amount, start_month, end_month } - const [impacts, setImpacts] = useState([]); - - // On open, if editing, fill in existing fields + /* ────────────── init / reset ────────────── */ useEffect(() => { - if (!show) return; // if modal is hidden, do nothing + if (!show) return; if (editMilestone) { - setTitle(editMilestone.title || ''); + setTitle(editMilestone.title || ''); setDescription(editMilestone.description || ''); - // If editing, you might fetch existing impacts from the server or they could be passed in - if (editMilestone.impacts) { - setImpacts(editMilestone.impacts); - } else { - // fetch from backend if needed - // e.g. GET /api/premium/milestones/:id/impacts - } + setImpacts(editMilestone.impacts || []); } else { - // Creating a new milestone - setTitle(''); - setDescription(''); - setImpacts([]); + setTitle(''); setDescription(''); setImpacts([]); } }, [show, editMilestone]); - // Handler: add a new blank impact - const handleAddImpact = () => { - setImpacts((prev) => [ + /* ────────────── helpers ────────────── */ + const addImpactRow = () => + setImpacts(prev => [ ...prev, { - impact_type: 'ONE_TIME', - direction: 'subtract', - amount: 0, - start_month: 0, - end_month: null + impact_type : 'cost', + frequency : 'ONE_TIME', + direction : 'subtract', + amount : 0, + start_date : '', // ISO yyyy‑mm‑dd + end_date : '' // blank β‡’ indefinite } ]); - }; - // Handler: update a single impact in the array - const handleImpactChange = (index, field, value) => { - setImpacts((prev) => { - const updated = [...prev]; - updated[index] = { ...updated[index], [field]: value }; - return updated; + const updateImpact = (idx, field, value) => + setImpacts(prev => { + const copy = [...prev]; + copy[idx] = { ...copy[idx], [field]: value }; + return copy; }); - }; - // Handler: remove an impact row - const handleRemoveImpact = (index) => { - setImpacts((prev) => prev.filter((_, i) => i !== index)); - }; + const removeImpact = idx => + setImpacts(prev => prev.filter((_, i) => i !== idx)); - // Handler: Save everything to the server - const handleSave = async () => { + /* ────────────── save ────────────── */ + async function handleSave() { try { - let milestoneId; - if (editMilestone) { - // 1) Update existing milestone - milestoneId = editMilestone.id; + /* 1️⃣ create OR update the milestone row */ + let milestoneId = editMilestone?.id; + if (milestoneId) { await authFetch(`api/premium/milestones/${milestoneId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - title, - description, - scenario_id: scenarioId, - // Possibly other fields - }) + method : 'PUT', + headers: { 'Content-Type':'application/json' }, + body : JSON.stringify({ title, description }) }); - // Then handle impacts below... } else { - // 1) Create new milestone const res = await authFetch('api/premium/milestones', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ + method : 'POST', + headers: { 'Content-Type':'application/json' }, + body : JSON.stringify({ title, description, - scenario_id: scenarioId + career_profile_id: scenarioId }) }); - if (!res.ok) throw new Error('Failed to create milestone'); - const created = await res.json(); - milestoneId = created.id; // assuming the response returns { id: newMilestoneId } + if (!res.ok) throw new Error('Milestone create failed'); + const json = await res.json(); + milestoneId = json.id ?? json[0]?.id; // array OR obj } - // 2) For the impacts, we can do a batch approach or individual calls - // For simplicity, let's do multiple POST calls - for (const impact of impacts) { - // If editing, you might do a PUT if the impact already has an id + /* 2️⃣ upsert each impact (one call per row) */ + for (const imp of impacts) { + const body = { + milestone_id : milestoneId, + impact_type : imp.impact_type, + frequency : imp.frequency, // ONE_TIME / MONTHLY + direction : imp.direction, + amount : parseFloat(imp.amount) || 0, + start_date : imp.start_date || null, + end_date : imp.frequency === 'MONTHLY' && imp.end_date + ? imp.end_date + : null + }; await authFetch('api/premium/milestone-impacts', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - milestone_id: milestoneId, - impact_type: impact.impact_type, - direction: impact.direction, - amount: parseFloat(impact.amount) || 0, - start_month: parseInt(impact.start_month, 10) || 0, - end_month: impact.end_month !== null - ? parseInt(impact.end_month, 10) - : null, - created_at: new Date().toISOString().slice(0, 10), - updated_at: new Date().toISOString().slice(0, 10) - }) + method : 'POST', + headers: { 'Content-Type':'application/json' }, + body : JSON.stringify(body) }); } - // Done, close modal - onClose(); + onClose(true); // ← parent will refetch } catch (err) { - console.error('Failed to save milestone + impacts:', err); - // Show some UI error if needed + console.error('Save failed:', err); + alert('Sorry, something went wrong – please try again.'); } - }; + } + /* ────────────── UI ────────────── */ if (!show) return null; - return (
-
+

{editMilestone ? 'Edit Milestone' : 'Add Milestone'}

-
- - setTitle(e.target.value)} - className="border w-full px-2 py-1" - /> -
+ {/* basic fields */} + + setTitle(e.target.value)} + className="border w-full px-2 py-1" + /> -
- -