266 lines
8.4 KiB
JavaScript
266 lines
8.4 KiB
JavaScript
// src/components/MilestoneAddModal.js
|
|
import React, { useState, useEffect } from 'react';
|
|
import authFetch from '../utils/authFetch.js';
|
|
|
|
const MilestoneAddModal = ({
|
|
show,
|
|
onClose,
|
|
defaultScenarioId,
|
|
scenarioId, // which scenario this milestone applies to
|
|
editMilestone, // if editing an existing milestone, pass its data
|
|
apiURL
|
|
}) => {
|
|
// Basic milestone fields
|
|
const [title, setTitle] = useState('');
|
|
const [description, setDescription] = 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
|
|
useEffect(() => {
|
|
if (!show) return; // if modal is hidden, do nothing
|
|
|
|
if (editMilestone) {
|
|
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
|
|
}
|
|
} else {
|
|
// Creating a new milestone
|
|
setTitle('');
|
|
setDescription('');
|
|
setImpacts([]);
|
|
}
|
|
}, [show, editMilestone]);
|
|
|
|
// Handler: add a new blank impact
|
|
const handleAddImpact = () => {
|
|
setImpacts((prev) => [
|
|
...prev,
|
|
{
|
|
impact_type: 'ONE_TIME',
|
|
direction: 'subtract',
|
|
amount: 0,
|
|
start_month: 0,
|
|
end_month: null
|
|
}
|
|
]);
|
|
};
|
|
|
|
// 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;
|
|
});
|
|
};
|
|
|
|
// Handler: remove an impact row
|
|
const handleRemoveImpact = (index) => {
|
|
setImpacts((prev) => prev.filter((_, i) => i !== index));
|
|
};
|
|
|
|
// Handler: Save everything to the server
|
|
const handleSave = async () => {
|
|
try {
|
|
let milestoneId;
|
|
if (editMilestone) {
|
|
// 1) Update existing milestone
|
|
milestoneId = editMilestone.id;
|
|
await authFetch(`${apiURL}/premium/milestones/${milestoneId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title,
|
|
description,
|
|
scenario_id: scenarioId,
|
|
// Possibly other fields
|
|
})
|
|
});
|
|
// Then handle impacts below...
|
|
} else {
|
|
// 1) Create new milestone
|
|
const res = await authFetch(`${apiURL}/premium/milestones`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title,
|
|
description,
|
|
scenario_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 }
|
|
}
|
|
|
|
// 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
|
|
await authFetch(`${apiURL}/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(),
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
});
|
|
}
|
|
|
|
// Done, close modal
|
|
onClose();
|
|
} catch (err) {
|
|
console.error('Failed to save milestone + impacts:', err);
|
|
// Show some UI error if needed
|
|
}
|
|
};
|
|
|
|
if (!show) return null;
|
|
|
|
return (
|
|
<div className="modal-backdrop">
|
|
<div className="modal-container">
|
|
<h2 className="text-xl font-bold mb-2">
|
|
{editMilestone ? 'Edit Milestone' : 'Add Milestone'}
|
|
</h2>
|
|
|
|
<div className="mb-3">
|
|
<label className="block font-semibold">Title</label>
|
|
<input
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
className="border w-full px-2 py-1"
|
|
/>
|
|
</div>
|
|
|
|
<div className="mb-3">
|
|
<label className="block font-semibold">Description</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
className="border w-full px-2 py-1"
|
|
/>
|
|
</div>
|
|
|
|
{/* Impacts Section */}
|
|
<h3 className="text-lg font-semibold mt-4">Financial Impacts</h3>
|
|
{impacts.map((impact, i) => (
|
|
<div key={i} className="border rounded p-2 my-2">
|
|
<div className="flex items-center justify-between">
|
|
<p>Impact #{i + 1}</p>
|
|
<button
|
|
className="text-red-500"
|
|
onClick={() => handleRemoveImpact(i)}
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
|
|
{/* Impact Type */}
|
|
<div className="mt-2">
|
|
<label className="block font-semibold">Type</label>
|
|
<select
|
|
value={impact.impact_type}
|
|
onChange={(e) =>
|
|
handleImpactChange(i, 'impact_type', e.target.value)
|
|
}
|
|
>
|
|
<option value="ONE_TIME">One-Time</option>
|
|
<option value="MONTHLY">Monthly</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Direction */}
|
|
<div className="mt-2">
|
|
<label className="block font-semibold">Direction</label>
|
|
<select
|
|
value={impact.direction}
|
|
onChange={(e) =>
|
|
handleImpactChange(i, 'direction', e.target.value)
|
|
}
|
|
>
|
|
<option value="add">Add (Income)</option>
|
|
<option value="subtract">Subtract (Expense)</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Amount */}
|
|
<div className="mt-2">
|
|
<label className="block font-semibold">Amount</label>
|
|
<input
|
|
type="number"
|
|
value={impact.amount}
|
|
onChange={(e) =>
|
|
handleImpactChange(i, 'amount', e.target.value)
|
|
}
|
|
className="border px-2 py-1 w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Start Month */}
|
|
<div className="mt-2">
|
|
<label className="block font-semibold">Start Month</label>
|
|
<input
|
|
type="number"
|
|
value={impact.start_month}
|
|
onChange={(e) =>
|
|
handleImpactChange(i, 'start_month', e.target.value)
|
|
}
|
|
className="border px-2 py-1 w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* End Month (for MONTHLY, can be null/blank if indefinite) */}
|
|
{impact.impact_type === 'MONTHLY' && (
|
|
<div className="mt-2">
|
|
<label className="block font-semibold">End Month (optional)</label>
|
|
<input
|
|
type="number"
|
|
value={impact.end_month || ''}
|
|
onChange={(e) =>
|
|
handleImpactChange(i, 'end_month', e.target.value || null)
|
|
}
|
|
className="border px-2 py-1 w-full"
|
|
placeholder="Leave blank for indefinite"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<button onClick={handleAddImpact} className="bg-gray-200 px-3 py-1 my-2">
|
|
+ Add Impact
|
|
</button>
|
|
|
|
{/* Modal Actions */}
|
|
<div className="flex justify-end mt-4">
|
|
<button className="mr-2" onClick={onClose}>
|
|
Cancel
|
|
</button>
|
|
<button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={handleSave}>
|
|
Save Milestone
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MilestoneAddModal;
|