diff --git a/.build.hash b/.build.hash index 716eaf2..c96630b 100644 --- a/.build.hash +++ b/.build.hash @@ -1 +1 @@ -b632ad41cfb05900be9a667c396e66a4dfb26320-22db19df6f582b90837f001b7bf6ead59acca441-e9eccd451b778829eb2f2c9752c670b707e1268b +afd62e0deab27814cfa0067f1fae1dc4ad79e7dd-22db19df6f582b90837f001b7bf6ead59acca441-e9eccd451b778829eb2f2c9752c670b707e1268b diff --git a/src/components/MilestoneDrawer.js b/src/components/MilestoneDrawer.js index 8a5e8c0..d0b8aa3 100644 --- a/src/components/MilestoneDrawer.js +++ b/src/components/MilestoneDrawer.js @@ -1,61 +1,59 @@ +// src/components/MilestoneDrawer.js import React, { useMemo, useState, useEffect } from 'react'; import { Button } from './ui/button.js'; import { Card, CardContent } from './ui/card.js'; -import { ChevronLeft, Check, Loader2 } from 'lucide-react'; +import { ChevronLeft, Check, Trash2, PencilLine, X } from 'lucide-react'; import { flattenTasks } from '../utils/taskHelpers.js'; import authFetch from '../utils/authFetch.js'; import format from 'date-fns/format'; -/* simple status → color map */ const pillStyle = { completed : 'bg-green-100 text-green-800', in_progress : 'bg-blue-100 text-blue-800', not_started : 'bg-gray-100 text-gray-700' }; +const statusLabel = { + not_started : 'Not started', + in_progress : 'In progress', + completed : 'Completed' +}; + +const nextStatus = { not_started:'in_progress', in_progress:'completed', completed:'not_started' }; + export default function MilestoneDrawer({ - milestone, // ← pass a single milestone object - milestones = [], // still needed to compute progress % + milestone, // single milestone object + milestones = [], // still available if you compute progress elsewhere open, onClose, onTaskToggle = () => {} }) { - - /* gather tasks progress for this milestone */ - const [tasks, setTasks] = useState( - milestone ? flattenTasks([milestone]) : [] - ); + // Local task list (flatten if your milestone.tasks has nested shape) + const [tasks, setTasks] = useState(milestone ? flattenTasks([milestone]) : []); + const [adding, setAdding] = useState(false); + const [editingId, setEditingId] = useState(null); + + const [draftNew, setDraftNew] = useState({ title:'', due_date:'', description:'' }); + const [draftEdit, setDraftEdit] = useState({ title:'', due_date:'', description:'' }); - // refresh local copy whenever the user selects a different milestone useEffect(() => { setTasks(milestone ? flattenTasks([milestone]) : []); + setAdding(false); + setEditingId(null); + setDraftNew({ title:'', due_date:'', description:'' }); }, [milestone]); + if (!open || !milestone) return null; + const done = tasks.filter(t => t.status === 'completed').length; const prog = tasks.length ? Math.round(100 * done / tasks.length) : 0; - if (!open || !milestone) return null; - async function toggle(t) { - - const next = { - not_started : 'in_progress', - in_progress : 'completed', - completed : 'not_started' // undo -}; + const newStatus = nextStatus[t.status] || 'not_started'; -const newStatus = next[t.status] || 'not_started'; - - - /* 1️⃣ optimistic local update */ - setTasks(prev => - prev.map(x => - x.id === t.id ? { ...x, status: newStatus } : x - ) - ); - - /* 2️⃣ inform parent so progress bars refresh elsewhere */ - onTaskToggle(t.id, newStatus); + // optimistic local update + setTasks(prev => prev.map(x => x.id === t.id ? { ...x, status:newStatus } : x)); + onTaskToggle(t.id, newStatus); await authFetch(`/api/premium/tasks/${t.id}`, { method : 'PUT', @@ -64,11 +62,82 @@ const newStatus = next[t.status] || 'not_started'; }); } - const statusLabel = { - not_started : 'Not started', - in_progress : 'In progress', - completed : 'Completed' -}; + async function createTask() { + const { title, due_date, description } = draftNew; + if (!title.trim()) return; + + const body = { + milestone_id: milestone.id, + title: title.trim(), + description: description || '', + due_date: due_date || null, + status: 'not_started' + }; + + // optimistic add (temporary id) + const tempId = `tmp-${Date.now()}`; + setTasks(prev => [...prev, { ...body, id: tempId }]); + setDraftNew({ title:'', due_date:'', description:'' }); + setAdding(false); + + const res = await authFetch('/api/premium/tasks', { + method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) + }); + + if (res.ok) { + const saved = await res.json(); + const real = Array.isArray(saved) ? saved[0] : saved; + // replace temp id with real id + setTasks(prev => prev.map(t => t.id === tempId ? { ...t, id: real.id } : t)); + } else { + // rollback on failure + setTasks(prev => prev.filter(t => t.id !== tempId)); + alert(await res.text()); + } + } + + function beginEdit(t) { + setEditingId(t.id); + setDraftEdit({ + title: t.title || '', + due_date: t.due_date ? String(t.due_date).slice(0,10) : '', + description: t.description || '' + }); + } + + function cancelEdit() { + setEditingId(null); + setDraftEdit({ title:'', due_date:'', description:'' }); + } + + async function saveEdit(id) { + const body = { + title: (draftEdit.title || '').trim(), + description: draftEdit.description || '', + due_date: draftEdit.due_date || null + }; + if (!body.title) return; + + // optimistic local update + setTasks(prev => prev.map(t => t.id === id ? { ...t, ...body } : t)); + setEditingId(null); + + const res = await authFetch(`/api/premium/tasks/${id}`, { + method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) + }); + if (!res.ok) alert(await res.text()); + } + + async function remove(id) { + // optimistic local delete + const prev = tasks; + setTasks(prev.filter(t => t.id !== id)); + const res = await authFetch(`/api/premium/tasks/${id}`, { method:'DELETE' }); + if (!res.ok) { + alert(await res.text()); + setTasks(prev); // rollback + } + } return (
{prog}% complete
{t.title}
- {t.due_date && ( -- {format(new Date(t.due_date), 'PP')} -
- )} -{t.title}
+- No tasks have been added to this milestone yet. -
+ {!tasks.length && !adding && ( +No tasks have been added to this milestone yet.
)} diff --git a/src/components/MilestoneEditModal.js b/src/components/MilestoneEditModal.js index 9c1c0d9..286445b 100644 --- a/src/components/MilestoneEditModal.js +++ b/src/components/MilestoneEditModal.js @@ -5,228 +5,405 @@ import InfoTooltip from './ui/infoTooltip.js'; import authFetch from '../utils/authFetch.js'; import MilestoneCopyWizard from './MilestoneCopyWizard.js'; -/* Helper ---------------------------------------------------- */ +/* Helpers ---------------------------------------------------- */ const toSqlDate = (v) => (v ? String(v).slice(0, 10) : ''); +const toDateOrNull = (v) => (v ? String(v).slice(0, 10) : null); export default function MilestoneEditModal({ careerProfileId, milestones: incomingMils = [], milestone: selectedMilestone, fetchMilestones, - onClose + onClose, }) { /* ───────────────── state */ - const [milestones, setMilestones] = useState(incomingMils); - const [editingId, setEditingId] = useState(null); - const [draft, setDraft] = useState({}); // id → {…fields} + const [milestones, setMilestones] = useState(incomingMils); + const [editingId, setEditingId] = useState(null); + + // draft maps milestoneId -> {title, description, date, progress, newSalary, impacts[], tasks[], isUniversal} + const [draft, setDraft] = useState({}); + + // Track original IDs so we can detect deletions on save const [originalImpactIdsMap, setOriginalImpactIdsMap] = useState({}); - const [addingNew, setAddingNew] = useState(false); - const [newMilestone, setNewMilestone] = useState({ - title:'', description:'', date:'', progress:0, newSalary:'', - impacts:[], isUniversal:0 + const [originalTaskIdsMap, setOriginalTaskIdsMap] = useState({}); + + const [addingNew, setAddingNew] = useState(false); + const [newMilestone, setNewMilestone] = useState({ + title: '', + description: '', + date: '', + progress: 0, + newSalary: '', + impacts: [], + tasks: [], + isUniversal: 0, }); - const [MilestoneCopyWizard, setMilestoneCopyWizard] = useState(null); + + const [copyWizardMilestone, setCopyWizardMilestone] = useState(null); const [isSavingEdit, setIsSavingEdit] = useState(false); - const [isSavingNew , setIsSavingNew ] = useState(false); + const [isSavingNew, setIsSavingNew] = useState(false); /* keep list in sync with prop */ - useEffect(()=> setMilestones(incomingMils), [incomingMils]); + useEffect(() => setMilestones(incomingMils), [incomingMils]); /* --------------------------------------------------------- * - * Load impacts for one milestone then open its accordion + * Load impacts + tasks for one milestone then open it * --------------------------------------------------------- */ - const openEditor = useCallback(async (m) => { + const openEditor = useCallback( + async (m) => { + setEditingId(m.id); - setEditingId(m.id); - const res = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`); - const json = res.ok ? await res.json() : { impacts:[] }; - const imps = (json.impacts||[]).map(i=>({ - id:i.id, - impact_type : i.impact_type||'ONE_TIME', - direction : i.direction||'subtract', - amount : i.amount||0, - start_date : toSqlDate(i.start_date), - end_date : toSqlDate(i.end_date) - })); + // Fetch impacts + const resImp = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`); + const jsonImp = resImp.ok ? await resImp.json() : { impacts: [] }; + const imps = (jsonImp.impacts || []).map((i) => ({ + id: i.id, + impact_type: i.impact_type || 'ONE_TIME', // 'salary' | 'ONE_TIME' | 'MONTHLY' + direction: i.impact_type === 'salary' ? 'add' : (i.direction || 'subtract'), + amount: i.amount || 0, + start_date: toSqlDate(i.start_date), + end_date: toSqlDate(i.end_date), + })); - setDraft(d => ({ ...d, - [m.id]: { - title : m.title||'', - description : m.description||'', - date : toSqlDate(m.date), - progress : m.progress||0, - newSalary : m.new_salary||'', - impacts : imps, - isUniversal : m.is_universal?1:0 - } - })); - setOriginalImpactIdsMap(p => ({ ...p, [m.id]: imps.map(i=>i.id)})); - }, [editingId]); + // Fetch tasks + const resTasks = await authFetch(`/api/premium/tasks?milestone_id=${m.id}`); + const jsonTasks = resTasks.ok ? await resTasks.json() : { tasks: [] }; + const tasks = (jsonTasks.tasks || []).map((t) => ({ + id: t.id, + title: t.title || '', + description: t.description || '', + due_date: toSqlDate(t.due_date), + status: t.status || 'not_started', // 'not_started' | 'in_progress' | 'completed' + })); - const handleAccordionClick = (m) => { + setDraft((d) => ({ + ...d, + [m.id]: { + title: m.title || '', + description: m.description || '', + date: toSqlDate(m.date), + progress: m.progress || 0, + newSalary: m.new_salary || '', + impacts: imps, + tasks, + isUniversal: m.is_universal ? 1 : 0, + }, + })); + setOriginalImpactIdsMap((p) => ({ ...p, [m.id]: imps.map((i) => i.id).filter(Boolean) })); + setOriginalTaskIdsMap((p) => ({ ...p, [m.id]: tasks.map((t) => t.id).filter(Boolean) })); + }, + [] + ); + + const handleAccordionClick = (m) => { if (editingId === m.id) { - setEditingId(null); // just close + setEditingId(null); } else { - openEditor(m); // open + fetch + openEditor(m); } }; - /* open editor automatically when parent passed selectedMilestone */ - useEffect(()=>{ if(selectedMilestone) openEditor(selectedMilestone)},[selectedMilestone,openEditor]); + // auto-open when a specific milestone is passed in + useEffect(() => { + if (selectedMilestone) openEditor(selectedMilestone); + }, [selectedMilestone, openEditor]); /* --------------------------------------------------------- * - * Handlers – shared small helpers + * Impact helpers * --------------------------------------------------------- */ const updateImpact = (mid, idx, field, value) => - setDraft(p => { - const d = p[mid]; if(!d) return p; - const copy = [...d.impacts]; copy[idx] = { ...copy[idx], [field]: value }; - return { ...p, [mid]: { ...d, impacts:copy }}; + setDraft((p) => { + const d = p[mid]; + if (!d) return p; + const copy = [...d.impacts]; + copy[idx] = { ...copy[idx], [field]: value }; + return { ...p, [mid]: { ...d, impacts: copy } }; }); const addImpactRow = (mid) => - setDraft(p=>{ - const d=p[mid]; if(!d) return p; - const blank = { impact_type:'ONE_TIME', direction:'subtract', amount:0, start_date:'', end_date:'' }; - return { ...p, [mid]: { ...d, impacts:[...d.impacts, blank]}}; + setDraft((p) => { + const d = p[mid]; + if (!d) return p; + const blank = { impact_type: 'ONE_TIME', direction: 'subtract', amount: 0, start_date: '', end_date: '' }; + return { ...p, [mid]: { ...d, impacts: [...d.impacts, blank] } }; }); - const removeImpactRow = (mid,idx)=> - setDraft(p=>{ - const d=p[mid]; if(!d) return p; - const c=[...d.impacts]; c.splice(idx,1); - return {...p,[mid]:{...d,impacts:c}}; + const removeImpactRow = (mid, idx) => + setDraft((p) => { + const d = p[mid]; + if (!d) return p; + const c = [...d.impacts]; + c.splice(idx, 1); + return { ...p, [mid]: { ...d, impacts: c } }; }); /* --------------------------------------------------------- * - * Persist edits (UPDATE / create / delete diff) + * Task helpers * --------------------------------------------------------- */ - async function saveMilestone(m){ - if(isSavingEdit) return; // guard - const d = draft[m.id]; if(!d) return; + const addTaskRow = (mid) => + setDraft((p) => { + const d = p[mid]; + if (!d) return p; + const blank = { id: null, title: '', description: '', due_date: '', status: 'not_started' }; + return { ...p, [mid]: { ...d, tasks: [...(d.tasks || []), blank] } }; + }); + + const updateTask = (mid, idx, field, value) => + setDraft((p) => { + const d = p[mid]; + if (!d) return p; + const copy = [...(d.tasks || [])]; + copy[idx] = { ...copy[idx], [field]: value }; + return { ...p, [mid]: { ...d, tasks: copy } }; + }); + + const removeTaskRow = (mid, idx) => + setDraft((p) => { + const d = p[mid]; + if (!d) return p; + const copy = [...(d.tasks || [])]; + copy.splice(idx, 1); + return { ...p, [mid]: { ...d, tasks: copy } }; + }); + + /* --------------------------------------------------------- * + * Persist edits (UPDATE / create / delete diff) + * --------------------------------------------------------- */ + async function saveMilestone(m) { + if (isSavingEdit) return; + const d = draft[m.id]; + if (!d) return; setIsSavingEdit(true); - /* header */ - const payload = { - milestone_type:'Financial', - title:d.title, description:d.description, date:toSqlDate(d.date), - career_profile_id:careerProfileId, progress:d.progress, - status:d.progress>=100?'completed':'planned', - new_salary:d.newSalary?parseFloat(d.newSalary):null, - is_universal:d.isUniversal - }; - const res = await authFetch(`/api/premium/milestones/${m.id}`,{ - method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) - }); - if(!res.ok){ alert('Save failed'); return;} - const saved = await res.json(); - - /* impacts diff */ - const originalIds = originalImpactIdsMap[m.id]||[]; - const currentIds = d.impacts.map(i=>i.id).filter(Boolean); - /* deletions */ - for(const id of originalIds.filter(x=>!currentIds.includes(x))){ - await authFetch(`/api/premium/milestone-impacts/${id}`,{method:'DELETE'}); - } - /* upserts */ - for(const imp of d.impacts){ - const body = { - milestone_id:saved.id, - impact_type:imp.impact_type, - direction:imp.impact_type==='salary'?'add':imp.direction, - amount:parseFloat(imp.amount)||0, - start_date:imp.start_date||null, - end_date:imp.end_date||null + try { + /* header */ + const payload = { + milestone_type: 'Financial', + title: d.title, + description: d.description, + date: toDateOrNull(d.date), + career_profile_id: careerProfileId, + progress: d.progress || 0, + status: (d.progress || 0) >= 100 ? 'completed' : 'planned', + new_salary: d.newSalary ? parseFloat(d.newSalary) : null, + is_universal: d.isUniversal ? 1 : 0, }; - if(imp.id){ - await authFetch(`/api/premium/milestone-impacts/${imp.id}`,{ - method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); - }else{ - await authFetch('/api/premium/milestone-impacts',{ - method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); + + const res = await authFetch(`/api/premium/milestones/${m.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + if (!res.ok) throw new Error(await res.text()); + const saved = await res.json(); + + /* impacts diff */ + const originalImpIds = originalImpactIdsMap[m.id] || []; + const currentImpIds = (d.impacts || []).map((i) => i.id).filter(Boolean); + + // deletions + for (const id of originalImpIds.filter((x) => !currentImpIds.includes(x))) { + await authFetch(`/api/premium/milestone-impacts/${id}`, { method: 'DELETE' }); } + + // upserts + for (const imp of d.impacts || []) { + const body = { + milestone_id: saved.id, + impact_type: imp.impact_type, + direction: imp.impact_type === 'salary' ? 'add' : imp.direction, + amount: parseFloat(imp.amount) || 0, + start_date: toDateOrNull(imp.start_date), + end_date: toDateOrNull(imp.end_date), + }; + if (imp.id) { + await authFetch(`/api/premium/milestone-impacts/${imp.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + } else { + await authFetch('/api/premium/milestone-impacts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + } + } + + /* tasks diff */ + const originalTaskIds = originalTaskIdsMap[m.id] || []; + const currentTaskIds = (d.tasks || []).map((t) => t.id).filter(Boolean); + + // deletions + for (const id of originalTaskIds.filter((x) => !currentTaskIds.includes(x))) { + await authFetch(`/api/premium/tasks/${id}`, { method: 'DELETE' }); + } + + // upserts + for (const t of d.tasks || []) { + const body = { + milestone_id: saved.id, + title: t.title || '', + description: t.description || '', + due_date: toDateOrNull(t.due_date), + status: t.status || 'not_started', + }; + if (t.id) { + await authFetch(`/api/premium/tasks/${t.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + } else { + await authFetch('/api/premium/tasks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + } + } + + await fetchMilestones(); + setEditingId(null); + onClose(true); + } catch (err) { + console.error('saveMilestone:', err); + alert(err.message || 'Save failed'); + } finally { + setIsSavingEdit(false); } - await fetchMilestones(); - setEditingId(null); - setIsSavingEdit(false); - onClose(true); } - async function deleteMilestone(m){ - if(!window.confirm(`Delete “${m.title}”?`)) return; - await authFetch(`/api/premium/milestones/${m.id}`,{method:'DELETE'}); + async function deleteMilestone(m) { + if (!window.confirm(`Delete “${m.title}”?`)) return; + await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' }); await fetchMilestones(); onClose(true); } /* --------------------------------------------------------- * - * New‑milestone helpers (create flow) + * New-milestone helpers (create flow) * --------------------------------------------------------- */ - const addBlankImpactToNew = ()=> setNewMilestone(n=>({ - ...n, impacts:[...n.impacts,{impact_type:'ONE_TIME',direction:'subtract',amount:0,start_date:'',end_date:''}] - })); - const updateNewImpact = (idx,field,val)=> setNewMilestone(n=>{ - const c=[...n.impacts]; c[idx]={...c[idx],[field]:val}; return {...n,impacts:c}; - }); - const removeNewImpact = (idx)=> setNewMilestone(n=>{ - const c=[...n.impacts]; c.splice(idx,1); return {...n,impacts:c}; - }); + const addBlankImpactToNew = () => + setNewMilestone((n) => ({ + ...n, + impacts: [...n.impacts, { impact_type: 'ONE_TIME', direction: 'subtract', amount: 0, start_date: '', end_date: '' }], + })); - async function saveNew(){ - if (isSavingNew) return; - if (!newMilestone.title.trim() || !newMilestone.date.trim()) { - alert('Need title & date'); return; - } - setIsSavingNew(true); - const toDate = (v) => (v ? String(v).slice(0,10) : null); - try { - const hdr = { - title: newMilestone.title, - description: newMilestone.description, - date: toDate(newMilestone.date), - career_profile_id: careerProfileId, - progress: newMilestone.progress, - status: newMilestone.progress >= 100 ? 'completed' : 'planned', - is_universal: newMilestone.isUniversal, - }; - const res = await authFetch('/api/premium/milestone', { - method: 'POST', - headers: { 'Content-Type':'application/json' }, - body: JSON.stringify(hdr) + const updateNewImpact = (idx, field, val) => + setNewMilestone((n) => { + const c = [...n.impacts]; + c[idx] = { ...c[idx], [field]: val }; + return { ...n, impacts: c }; }); - if (!res.ok) throw new Error(await res.text()); - const createdJson = await res.json(); - const created = Array.isArray(createdJson) ? createdJson[0] : createdJson; - if (!created || !created.id) throw new Error('Milestone create failed — no id returned'); - // Save any non-empty impact rows - for (const imp of newMilestone.impacts) { - if (!imp) continue; - const hasAnyField = (imp.amount || imp.start_date || imp.end_date || imp.impact_type); - if (!hasAnyField) continue; - const ibody = { - milestone_id: created.id, - impact_type : imp.impact_type, - direction : imp.impact_type === 'salary' ? 'add' : imp.direction, - amount : parseFloat(imp.amount) || 0, - start_date : toDate(imp.start_date), - end_date : toDate(imp.end_date), - }; - const ir = await authFetch('/api/premium/milestone-impacts', { - method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(ibody) - }); - if (!ir.ok) throw new Error(await ir.text()); + const removeNewImpact = (idx) => + setNewMilestone((n) => { + const c = [...n.impacts]; + c.splice(idx, 1); + return { ...n, impacts: c }; + }); + + const addBlankTaskToNew = () => + setNewMilestone((n) => ({ + ...n, + tasks: [...n.tasks, { title: '', description: '', due_date: '', status: 'not_started' }], + })); + + const updateNewTask = (idx, field, val) => + setNewMilestone((n) => { + const c = [...n.tasks]; + c[idx] = { ...c[idx], [field]: val }; + return { ...n, tasks: c }; + }); + + const removeNewTask = (idx) => + setNewMilestone((n) => { + const c = [...n.tasks]; + c.splice(idx, 1); + return { ...n, tasks: c }; + }); + + async function saveNew() { + if (isSavingNew) return; + if (!newMilestone.title.trim() || !newMilestone.date.trim()) { + alert('Need title & date'); + return; } + setIsSavingNew(true); - await fetchMilestones(); - setAddingNew(false); - onClose(true); - } catch (err) { - console.error('saveNew:', err); - alert(err.message || 'Failed to save milestone'); - } finally { - setIsSavingNew(false); - } + try { + const hdr = { + title: newMilestone.title, + description: newMilestone.description, + date: toDateOrNull(newMilestone.date), + career_profile_id: careerProfileId, + progress: newMilestone.progress || 0, + status: (newMilestone.progress || 0) >= 100 ? 'completed' : 'planned', + is_universal: newMilestone.isUniversal ? 1 : 0, + }; + + const res = await authFetch('/api/premium/milestone', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(hdr), + }); + if (!res.ok) throw new Error(await res.text()); + const createdJson = await res.json(); + const created = Array.isArray(createdJson) ? createdJson[0] : createdJson; + if (!created || !created.id) throw new Error('Milestone create failed — no id returned'); + + // Save impacts + for (const imp of newMilestone.impacts) { + if (!imp) continue; + const hasAnyField = imp.amount || imp.start_date || imp.end_date || imp.impact_type; + if (!hasAnyField) continue; + const ibody = { + milestone_id: created.id, + impact_type: imp.impact_type, + direction: imp.impact_type === 'salary' ? 'add' : imp.direction, + amount: parseFloat(imp.amount) || 0, + start_date: toDateOrNull(imp.start_date), + end_date: toDateOrNull(imp.end_date), + }; + const ir = await authFetch('/api/premium/milestone-impacts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(ibody), + }); + if (!ir.ok) throw new Error(await ir.text()); + } + + // Save tasks + for (const t of newMilestone.tasks) { + if (!t) continue; + const hasAnyField = t.title || t.description || t.due_date; + if (!hasAnyField) continue; + const tbody = { + milestone_id: created.id, + title: t.title || '', + description: t.description || '', + due_date: toDateOrNull(t.due_date), + status: t.status || 'not_started', + }; + const tr = await authFetch('/api/premium/tasks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(tbody), + }); + if (!tr.ok) throw new Error(await tr.text()); + } + + await fetchMilestones(); + setAddingNew(false); + onClose(true); + } catch (err) { + console.error('saveNew:', err); + alert(err.message || 'Failed to save milestone'); + } finally { + setIsSavingNew(false); + } } /* ══════════════════════════════════════════════════════════════ */ @@ -239,43 +416,44 @@ export default function MilestoneEditModal({- Track important events and their financial impact on this scenario. -
+Track important events and their financial impact on this scenario.
Use salary for annual income changes; monthly for recurring amounts.
Break this milestone into concrete steps.
+ +