dev1/src/components/MilestoneAddModal.js

259 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/components/MilestoneAddModal.js
import React, { useState, useEffect } from 'react';
import authFetch from '../utils/authFetch.js';
/* ─────────────────────────────────────────────────────────
CONSTANTS
───────────────────────────────────────────────────────── */
const IMPACT_TYPES = ['salary', 'cost', 'tuition', 'note'];
const FREQ_OPTIONS = ['ONE_TIME', 'MONTHLY'];
export default function MilestoneAddModal({
show,
onClose,
scenarioId, // active scenario UUID
editMilestone = null // pass full row when editing
}) {
/* ────────────── state ────────────── */
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [impacts, setImpacts] = useState([]);
/* ────────────── init / reset ────────────── */
useEffect(() => {
if (!show) return;
if (editMilestone) {
setTitle(editMilestone.title || '');
setDescription(editMilestone.description || '');
setImpacts(editMilestone.impacts || []);
} else {
setTitle(''); setDescription(''); setImpacts([]);
}
}, [show, editMilestone]);
/* ────────────── helpers ────────────── */
const addImpactRow = () =>
setImpacts(prev => [
...prev,
{
impact_type : 'cost',
frequency : 'ONE_TIME',
direction : 'subtract',
amount : 0,
start_date : '', // ISO yyyymmdd
end_date : '' // blank ⇒ indefinite
}
]);
const updateImpact = (idx, field, value) =>
setImpacts(prev => {
const copy = [...prev];
copy[idx] = { ...copy[idx], [field]: value };
return copy;
});
const removeImpact = idx =>
setImpacts(prev => prev.filter((_, i) => i !== idx));
/* ────────────── save ────────────── */
async function handleSave() {
try {
/* 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 })
});
} else {
const res = await authFetch('api/premium/milestones', {
method : 'POST',
headers: { 'Content-Type':'application/json' },
body : JSON.stringify({
title,
description,
career_profile_id: scenarioId
})
});
if (!res.ok) throw new Error('Milestone create failed');
const json = await res.json();
milestoneId = json.id ?? json[0]?.id; // array OR obj
}
/* 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(body)
});
}
onClose(true); // ← parent will refetch
} catch (err) {
console.error('Save failed:', err);
alert('Sorry, something went wrong please try again.');
}
}
/* ────────────── UI ────────────── */
if (!show) return null;
return (
<div className="modal-backdrop">
<div className="modal-container w-full max-w-lg">
<h2 className="text-xl font-bold mb-2">
{editMilestone ? 'Edit Milestone' : 'Add Milestone'}
</h2>
{/* basic fields */}
<label className="block font-semibold mt-2">Title</label>
<input
value={title}
onChange={e => setTitle(e.target.value)}
className="border w-full px-2 py-1"
/>
<label className="block font-semibold mt-4">Description</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={3}
className="border w-full px-2 py-1"
/>
{/* impacts */}
<h3 className="text-lg font-semibold mt-6">FinancialImpacts</h3>
{impacts.map((imp, i) => (
<div key={i} className="border rounded p-3 mt-4 space-y-2">
<div className="flex justify-between items-center">
<span className="font-medium">Impact #{i + 1}</span>
<button
className="text-red-600 text-sm"
onClick={() => removeImpact(i)}
>
Remove
</button>
</div>
{/* type */}
<div>
<label className="block text-sm font-semibold">Type</label>
<select
value={imp.impact_type}
onChange={e => updateImpact(i, 'impact_type', e.target.value)}
className="border px-2 py-1 w-full"
>
{IMPACT_TYPES.map(t => (
<option key={t} value={t}>
{t === 'salary' ? 'Salary change'
: t === 'cost' ? 'Cost / expense'
: t.charAt(0).toUpperCase() + t.slice(1)}
</option>
))}
</select>
</div>
{/* frequency */}
<div>
<label className="block text-sm font-semibold">Frequency</label>
<select
value={imp.frequency}
onChange={e => updateImpact(i, 'frequency', e.target.value)}
className="border px-2 py-1 w-full"
>
<option value="ONE_TIME">Onetime</option>
<option value="MONTHLY">Monthly (recurring)</option>
</select>
</div>
{/* direction */}
<div>
<label className="block text-sm font-semibold">Direction</label>
<select
value={imp.direction}
onChange={e => updateImpact(i, 'direction', e.target.value)}
className="border px-2 py-1 w-full"
>
<option value="add">Add (income)</option>
<option value="subtract">Subtract (expense)</option>
</select>
</div>
{/* amount */}
<div>
<label className="block text-sm font-semibold">Amount ($)</label>
<input
type="number"
value={imp.amount}
onChange={e => updateImpact(i, 'amount', e.target.value)}
className="border px-2 py-1 w-full"
/>
</div>
{/* dates */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold">Start date</label>
<input
type="date"
value={imp.start_date}
onChange={e => updateImpact(i, 'start_date', e.target.value)}
className="border px-2 py-1 w-full"
/>
</div>
{imp.frequency === 'MONTHLY' && (
<div>
<label className="block text-sm font-semibold">
End date (optional)
</label>
<input
type="date"
value={imp.end_date || ''}
onChange={e => updateImpact(i, 'end_date', e.target.value)}
className="border px-2 py-1 w-full"
/>
</div>
)}
</div>
</div>
))}
<button
onClick={addImpactRow}
className="bg-gray-200 px-4 py-1 rounded mt-4"
>
+ Add impact
</button>
{/* actions */}
<div className="flex justify-end gap-3 mt-6">
<button onClick={() => onClose(false)} className="px-4 py-2">
Cancel
</button>
<button
onClick={handleSave}
className="bg-blue-600 text-white px-5 py-2 rounded"
>
Save
</button>
</div>
</div>
</div>
);
}