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 (
@@ -85,57 +154,135 @@ const newStatus = next[t.status] || 'not_started';

)}
+ {/* Body */} - {/* Progress bar */} + {/* Progress */}

{prog}% complete

+ {/* Add composer */} + {adding && ( +
+
+ + setDraftNew(d => ({ ...d, title:e.target.value }))} + placeholder="What needs to be done?" + /> +
+
+
+ + setDraftNew(d => ({ ...d, due_date:e.target.value }))} + /> +
+
+ + +
+
+
+ +