// src/components/MilestoneEditModal.js import React, { useState, useEffect, useCallback } from 'react'; import { Button } from './ui/button.js'; import InfoTooltip from './ui/infoTooltip.js'; import authFetch from '../utils/authFetch.js'; import MilestoneCopyWizard from './MilestoneCopyWizard.js'; /* Helper ---------------------------------------------------- */ const toSqlDate = (v) => (v ? String(v).slice(0, 10) : ''); export default function MilestoneEditModal({ careerProfileId, milestones: incomingMils = [], milestone: selectedMilestone, fetchMilestones, onClose }) { /* ───────────────── state */ const [milestones, setMilestones] = useState(incomingMils); const [editingId, setEditingId] = useState(null); const [draft, setDraft] = useState({}); // id → {…fields} const [originalImpactIdsMap, setOriginalImpactIdsMap] = useState({}); const [addingNew, setAddingNew] = useState(false); const [newMilestone, setNewMilestone] = useState({ title:'', description:'', date:'', progress:0, newSalary:'', impacts:[], isUniversal:0 }); const [MilestoneCopyWizard, setMilestoneCopyWizard] = useState(null); const [isSavingEdit, setIsSavingEdit] = useState(false); const [isSavingNew , setIsSavingNew ] = useState(false); /* keep list in sync with prop */ useEffect(()=> setMilestones(incomingMils), [incomingMils]); /* --------------------------------------------------------- * * Load impacts for one milestone then open its accordion * --------------------------------------------------------- */ const openEditor = useCallback(async (m) => { 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) })); 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]); const handleAccordionClick = (m) => { if (editingId === m.id) { setEditingId(null); // just close } else { openEditor(m); // open + fetch } }; /* open editor automatically when parent passed selectedMilestone */ useEffect(()=>{ if(selectedMilestone) openEditor(selectedMilestone)},[selectedMilestone,openEditor]); /* --------------------------------------------------------- * * Handlers – shared small 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 }}; }); 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]}}; }); 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) * --------------------------------------------------------- */ async function saveMilestone(m){ if(isSavingEdit) return; // guard 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 }; 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)}); } } 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'}); await fetchMilestones(); onClose(true); } /* --------------------------------------------------------- * * 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}; }); async function saveNew(){ if(isSavingNew) return; if(!newMilestone.title.trim()||!newMilestone.date.trim()){ alert('Need title & date'); return; } setIsSavingNew(true); const hdr = { title:newMilestone.title, description:newMilestone.description, date:toSqlDate(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 created = Array.isArray(await res.json())? (await res.json())[0]:await res.json(); for(const imp of newMilestone.impacts){ const body = { milestone_id:created.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 }; await authFetch('/api/premium/milestone-impacts',{ method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); } await fetchMilestones(); setAddingNew(false); onClose(true); } /* ══════════════════════════════════════════════════════════════ */ /* RENDER */ /* ══════════════════════════════════════════════════════════════ */ return (
Track important events and their financial impact on this scenario.
Use salary for annual income changes; monthly for recurring amounts.