From fa10761f80e3d878a9577a1f402ce4f0de9b35e8 Mon Sep 17 00:00:00 2001 From: Roman Mazuryk Date: Mon, 29 Jun 2026 11:44:22 +0200 Subject: [PATCH 1/5] =?UTF-8?q?docs(decisions):=20ADR-007=20=E2=80=94=20pr?= =?UTF-8?q?oject=20data=20model,=20milestones-only,=20sprint=20as=20separa?= =?UTF-8?q?te=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unblocks the edit-project-identity-strip session. Documents the vocabulary split problem (tasks vs milestones across surfaces) and the three-session plan to resolve it: session 1 strips the modal, session 2 adds Sprint, session 3 cleans up vocabulary. Co-Authored-By: Claude Sonnet 4.6 --- docs/DECISIONS_LOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/DECISIONS_LOG.md b/docs/DECISIONS_LOG.md index 3fb64ce..ef9b739 100644 --- a/docs/DECISIONS_LOG.md +++ b/docs/DECISIONS_LOG.md @@ -111,3 +111,24 @@ Architecture Decision Records (ADR-style). Each entry explains a real choice mad - The WORKFLOW_AUTOMATION_PLAYBOOK.md is now the primary artifact — the app itself is the worked example. **Revisit trigger:** Never, unless the project changes hands or purpose. + +--- + +## ADR-007: Project data model — milestones only, sprint as separate object + +**Date:** 2026-06-29 +**Status:** Accepted + +**Context:** The detail Sheet shows both "tasks" and "milestones" counts; the Edit modal shows neither, only a free-text "Weekly Outcome" field. Vocabulary is split across surfaces, blocking any coherent UI for progress tracking. The Edit modal conflated three cadences (identity, lifecycle, weekly operations) in one form, making every weekly edit scroll past rarely-changed identity fields. + +**Decision:** +- Project hierarchy: **Project → Milestones** (no tasks layer). A Milestone is a binary done/not-done outcome with an optional target date. Type: `{ id, title, done, targetDate?, completedAt? }`. +- Sprint is a **separate object**, soft-linked from sprint outcomes to milestones via optional `linked_milestone?: string`. Sprint data model and persistence are deferred to session 2; this session does not introduce it. +- "Tasks" vocabulary is removed from all UI surfaces targeting the projects domain. Existing fields that conceptually represent tasks are either renamed to milestones or left untouched pending session 3 (the milestones UI session). + +**Consequences:** +- Session 1 (this session): removes Weekly Outcome from Edit modal, moves Links to the detail Sheet, strips Edit modal to identity-only fields (Title, Objective, Duration, Color, Status). +- Session 2: introduces Sprint type + persistence + Sheet block. +- Session 3: milestones UI overhaul + vocabulary cleanup across surfaces. + +**Revisit trigger:** Sprint data model introduced in session 2 conflicts with this structure, or session 3 vocabulary cleanup reveals that the `Project → Milestones` hierarchy is insufficient for the actual tracking workflow. From 85a6caec05ccc293d36c54df9e8f982e96da2362 Mon Sep 17 00:00:00 2001 From: Roman Mazuryk Date: Mon, 29 Jun 2026 11:47:20 +0200 Subject: [PATCH 2/5] =?UTF-8?q?feat(projects):=20=C2=A71=20=E2=80=94=20rem?= =?UTF-8?q?ove=20Weekly=20Outcome=20and=20Links=20from=20Edit=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ADR-007: identity edits monthly, sprint/links change weekly. Conflating them forced every weekly edit to scroll past identity fields. Weekly Outcome and Links are removed from the modal; the type field stays for session-2 migration. Links move to the Sheet in §3. Active-project validation for weeklyOutcome is also removed. Co-Authored-By: Claude Sonnet 4.6 --- components/modules/projects-module.tsx | 93 -------------------------- 1 file changed, 93 deletions(-) diff --git a/components/modules/projects-module.tsx b/components/modules/projects-module.tsx index 2dcbf60..ba6323e 100644 --- a/components/modules/projects-module.tsx +++ b/components/modules/projects-module.tsx @@ -117,15 +117,11 @@ export function ProjectsModule() { const [editingId, setEditingId] = useState(null) const [title, setTitle] = useState("") const [objective, setObjective] = useState("") - const [weeklyOutcome, setWeeklyOutcome] = useState("") const [status, setStatus] = useState("active") const [color, setColor] = useState(DEFAULT_PROJECT_COLOR) const [milestones, setMilestones] = useState("") const [startDate, setStartDate] = useState("") const [endDate, setEndDate] = useState("") - const [newLinkLabel, setNewLinkLabel] = useState("") - const [newLinkUrl, setNewLinkUrl] = useState("") - const [newLinks, setNewLinks] = useState>([]) const [formError, setFormError] = useState(null) // Module state @@ -160,15 +156,11 @@ export function ProjectsModule() { setEditingId(null) setTitle("") setObjective("") - setWeeklyOutcome("") setStatus("active") setColor(DEFAULT_PROJECT_COLOR) setStartDate(defaultWeekRange.start) setEndDate(defaultWeekRange.end) setMilestones("") - setNewLinkLabel("") - setNewLinkUrl("") - setNewLinks([]) setFormError(null) setOpen(true) } @@ -177,44 +169,18 @@ export function ProjectsModule() { setEditingId(project.id) setTitle(project.title) setObjective(project.objective) - setWeeklyOutcome(project.weeklyOutcome ?? "") setStatus(getProjectStatus(project)) setColor(normalizeProjectColor(project.color)) setStartDate(project.weekStartISO) setEndDate(project.weekEndISO) setMilestones(project.milestones.map((m) => m.title).join(", ")) - setNewLinkLabel("") - setNewLinkUrl("") - setNewLinks(getProjectLinks(project)) setFormError(null) setOpen(true) } - function addStagedLink() { - const url = normalizeUrl(newLinkUrl) - if (!url) return - const label = newLinkLabel.trim() || "Link" - setNewLinks((prev) => [...prev, { label, url }]) - setNewLinkLabel("") - setNewLinkUrl("") - } - function saveProject() { if (!title.trim()) return - const normalizedOutcome = weeklyOutcome.trim() - if (status === "active" && !normalizedOutcome) { - setFormError("Active projects must have exactly one weekly outcome.") - return - } - const draftUrl = normalizeUrl(newLinkUrl) - const draftLinks = draftUrl - ? [...newLinks, { label: newLinkLabel.trim() || "Link", url: draftUrl }] - : newLinks - const normalizedLinks = draftLinks - .map((link) => ({ label: link.label.trim() || "Link", url: normalizeUrl(link.url) })) - .filter((link) => Boolean(link.url)) const selectedColor = normalizeProjectColor(color) - const weekStartISO = startDate || defaultWeekRange.start const weekEndISO = endDate || defaultWeekRange.end if (!editingId) { @@ -222,26 +188,20 @@ export function ProjectsModule() { title: title.trim(), objective: objective.trim() || "Project objective", showOnTimeline: true, - weeklyOutcome: normalizedOutcome || undefined, status: status ?? "active", weekStartISO, weekEndISO, color: selectedColor, - url: normalizedLinks[0]?.url, - links: normalizedLinks.length > 0 ? normalizedLinks : undefined, milestones: parseMilestones(milestones), }) } else { updateProject(editingId, { title: title.trim(), objective: objective.trim() || "Project objective", - weeklyOutcome: normalizedOutcome || undefined, status: status ?? "active", weekStartISO, weekEndISO, color: selectedColor, - url: normalizedLinks[0]?.url, - links: normalizedLinks.length > 0 ? normalizedLinks : undefined, }) } setFormError(null) @@ -313,18 +273,6 @@ export function ProjectsModule() { setObjective(e.target.value)} /> -
- - setWeeklyOutcome(e.target.value)} - placeholder="One concrete result for this week" - /> -

- Active projects require exactly one weekly outcome. -

-
setNewLinkLabel(e.target.value)} - placeholder="Label (e.g. Figma)" - className="sm:w-44 sm:flex-none" - /> - setNewLinkUrl(e.target.value)} - placeholder="https://..." - className="min-w-0" - /> - -
- {newLinks.length > 0 && ( -
- {newLinks.map((link, index) => ( -
- - {link.label}: {link.url} - - -
- ))} -
- )} - {!editingId ? (
From 009e260469a5068b574b52cd0777f6ce3b59256a Mon Sep 17 00:00:00 2001 From: Roman Mazuryk Date: Mon, 29 Jun 2026 12:20:25 +0200 Subject: [PATCH 3/5] =?UTF-8?q?feat(projects):=20=C2=A72=20=E2=80=94=20Cal?= =?UTF-8?q?endar=20pickers,=20color=20swatches,=20status=20ToggleGroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Duration: native → shadcn Calendar+Popover, format "dd MMM yyyy", end-after-start validation with inline error - Color: color input + hex text → single row of 8 preset swatches with ring-on-active selection; persists same color hex field - Status: Select dropdown → ToggleGroup (4 options, full-width, single-select) using existing STATUS_SECTIONS constant - Remove Select import; add Calendar, Popover, ToggleGroup imports Co-Authored-By: Claude Sonnet 4.6 --- components/modules/projects-module.tsx | 135 ++++++++++++++++--------- 1 file changed, 89 insertions(+), 46 deletions(-) diff --git a/components/modules/projects-module.tsx b/components/modules/projects-module.tsx index ba6323e..197f1af 100644 --- a/components/modules/projects-module.tsx +++ b/components/modules/projects-module.tsx @@ -9,15 +9,17 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { Calendar } from "@/components/ui/calendar" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Progress } from "@/components/ui/progress" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Sheet, SheetContent, SheetClose, SheetTitle } from "@/components/ui/sheet" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" import { TruncatedTooltip } from "@/components/ui/truncated-tooltip" -import { AlertTriangle, Check, ChevronRight, ExternalLink, MoreHorizontal, Pencil, Plus, Trash2, X } from "lucide-react" +import { AlertTriangle, CalendarIcon, Check, ChevronRight, ExternalLink, MoreHorizontal, Pencil, Plus, Trash2, X } from "lucide-react" import type { Project, ProjectMilestone, ProjectStatus, Task } from "@/lib/types" import { ProjectsTimelineChart } from "./projects-timeline-chart" @@ -30,6 +32,17 @@ const LEGACY_COLOR_MAP: Record = { "bg-chart-5": "#a855f7", } +const PRESET_COLORS = [ + { name: "blue", value: "#3b82f6" }, + { name: "purple", value: "#8b5cf6" }, + { name: "yellow", value: "#eab308" }, + { name: "green", value: "#22c55e" }, + { name: "pink", value: "#ec4899" }, + { name: "orange", value: "#f97316" }, + { name: "red", value: "#ef4444" }, + { name: "slate", value: "#64748b" }, +] + const STATUS_SECTIONS: Array<{ id: ProjectStatus; label: string }> = [ { id: "active", label: "Active" }, { id: "paused", label: "Paused" }, @@ -123,6 +136,8 @@ export function ProjectsModule() { const [startDate, setStartDate] = useState("") const [endDate, setEndDate] = useState("") const [formError, setFormError] = useState(null) + const [startDateOpen, setStartDateOpen] = useState(false) + const [endDateOpen, setEndDateOpen] = useState(false) // Module state const [view, setView] = useState<"list" | "timeline">("list") @@ -183,6 +198,10 @@ export function ProjectsModule() { const selectedColor = normalizeProjectColor(color) const weekStartISO = startDate || defaultWeekRange.start const weekEndISO = endDate || defaultWeekRange.end + if (weekStartISO > weekEndISO) { + setFormError("End date must be on or after start date.") + return + } if (!editingId) { addProject({ title: title.trim(), @@ -274,56 +293,80 @@ export function ProjectsModule() { setObjective(e.target.value)} />
- - + + { if (v) setStatus(v as Project["status"]) }} + className="mt-1.5 w-full" + > + {STATUS_SECTIONS.map((s) => ( + + {s.label} + + ))} +
- -
- setStartDate(e.target.value)} - className="flex-1 text-xs" - aria-label="Start date" - /> + +
+ + + + + + { + if (date) { setStartDate(format(date, "yyyy-MM-dd")); setStartDateOpen(false) } + }} + /> + + - setEndDate(e.target.value)} - className="flex-1 text-xs" - aria-label="End date" - /> + + + + + + { + if (date) { setEndDate(format(date, "yyyy-MM-dd")); setEndDateOpen(false) } + }} + /> + +
- -
- setColor(e.target.value)} - className="h-10 w-14 cursor-pointer p-1" - aria-label="Pick project card color" - /> - setColor(e.target.value)} - placeholder="#3b82f6" - className="font-mono text-xs" - /> + +
+ {PRESET_COLORS.map((c) => { + const active = normalizeProjectColor(color) === c.value + return ( +
{!editingId ? ( From d7a265c0ed81de498a99c563eff8855e50bc8dac Mon Sep 17 00:00:00 2001 From: Roman Mazuryk Date: Mon, 29 Jun 2026 13:50:59 +0200 Subject: [PATCH 4/5] =?UTF-8?q?feat(projects):=20=C2=A73=20=E2=80=94=20mov?= =?UTF-8?q?e=20Links=20from=20modal=20to=20Sheet=20(inline-editable)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Links now live on the detail Sheet below Milestones with: - + Add affordance → inline two-field row (Label / URL) + Save/Cancel - Each row: link icon, label as clickable text, URL in tooltip on hover - Hover reveals edit (pencil) and delete (trash) icons; delete requires a small inline confirm (✓/✕) instead of a modal - Empty state: "No links yet — add one." - Existing links survive modal saves since updateProject no longer touches url/links fields when editing identity Co-Authored-By: Claude Sonnet 4.6 --- components/modules/projects-module.tsx | 206 ++++++++++++++++++++++--- 1 file changed, 187 insertions(+), 19 deletions(-) diff --git a/components/modules/projects-module.tsx b/components/modules/projects-module.tsx index 197f1af..c947b6a 100644 --- a/components/modules/projects-module.tsx +++ b/components/modules/projects-module.tsx @@ -18,8 +18,9 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Progress } from "@/components/ui/progress" import { Sheet, SheetContent, SheetClose, SheetTitle } from "@/components/ui/sheet" import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { TruncatedTooltip } from "@/components/ui/truncated-tooltip" -import { AlertTriangle, CalendarIcon, Check, ChevronRight, ExternalLink, MoreHorizontal, Pencil, Plus, Trash2, X } from "lucide-react" +import { AlertTriangle, CalendarIcon, Check, ChevronRight, ExternalLink, Link as LinkIcon, MoreHorizontal, Pencil, Plus, Trash2, X } from "lucide-react" import type { Project, ProjectMilestone, ProjectStatus, Task } from "@/lib/types" import { ProjectsTimelineChart } from "./projects-timeline-chart" @@ -461,6 +462,7 @@ export function ProjectsModule() { project={selectedProject} tasks={tasks} onEdit={openEditDialog} + onUpdateProject={updateProject} onStatusChange={(id, nextStatus) => updateProject(id, { status: nextStatus })} onDelete={deleteProject} editingMilestone={editingMilestone} @@ -635,6 +637,7 @@ function ProjectDetailPanel({ project, tasks, onEdit, + onUpdateProject, onStatusChange, onDelete, editingMilestone, @@ -653,6 +656,7 @@ function ProjectDetailPanel({ project: Project tasks: Task[] onEdit: (project: Project) => void + onUpdateProject: (id: string, updates: Partial) => void onStatusChange: (id: string, status: ProjectStatus) => void onDelete: (id: string) => void editingMilestone: { projectId: string; milestoneId: string } | null @@ -669,6 +673,13 @@ function ProjectDetailPanel({ onToggleTask: (taskId: string) => void }) { const [showAddMilestone, setShowAddMilestone] = useState(false) + const [showAddLink, setShowAddLink] = useState(false) + const [addLinkLabel, setAddLinkLabel] = useState("") + const [addLinkUrl, setAddLinkUrl] = useState("") + const [editingLinkIdx, setEditingLinkIdx] = useState(null) + const [editingLinkLabel, setEditingLinkLabel] = useState("") + const [editingLinkUrl, setEditingLinkUrl] = useState("") + const [deleteLinkIdx, setDeleteLinkIdx] = useState(null) const projectTasks = tasks.filter((t) => t.linkedProjectId === project.id) const completedTasks = projectTasks.filter((t) => t.completed).length @@ -685,13 +696,41 @@ function ProjectDetailPanel({ const weeklyLines = project.weeklyOutcome?.trim() ? project.weeklyOutcome.trim().split("\n").filter((l) => l.trim()) : [] - const links = getProjectLinks(project) function commitAddMilestone() { handleAddMilestone(project.id) setShowAddMilestone(false) } + const projectLinks = getProjectLinks(project) + + function commitAddLink() { + const url = normalizeUrl(addLinkUrl) + if (!url) return + const label = addLinkLabel.trim() || "Link" + const next = [...projectLinks, { label, url }] + onUpdateProject(project.id, { links: next, url: next[0]?.url }) + setAddLinkLabel("") + setAddLinkUrl("") + setShowAddLink(false) + } + + function commitEditLink(idx: number) { + const url = normalizeUrl(editingLinkUrl) + if (!url) return + const next = projectLinks.map((l, i) => + i === idx ? { label: editingLinkLabel.trim() || "Link", url } : l + ) + onUpdateProject(project.id, { links: next, url: next[0]?.url }) + setEditingLinkIdx(null) + } + + function commitDeleteLink(idx: number) { + const next = projectLinks.filter((_, i) => i !== idx) + onUpdateProject(project.id, { links: next.length > 0 ? next : undefined, url: next[0]?.url }) + setDeleteLinkIdx(null) + } + return (
{/* Header: ● title · status line · ⋯ · ✕ */} @@ -987,26 +1026,155 @@ function ProjectDetailPanel({ ) })() : null} - {/* Links */} - {links.length > 0 ? ( -
+ {/* Links — inline editable */} +
+

Links

-
- {links.map((link, i) => ( - - - {link.label || "Link"} - - ))} + {!showAddLink ? ( + + ) : null} +
+ + {showAddLink ? ( +
+
+ setAddLinkLabel(e.target.value)} + placeholder="Label" + className="h-7 text-xs" + /> + setAddLinkUrl(e.target.value)} + placeholder="https://..." + className="h-7 flex-1 text-xs" + onKeyDown={(e) => { + if (e.key === "Enter") commitAddLink() + if (e.key === "Escape") setShowAddLink(false) + }} + autoFocus + /> +
+
+ + +
+ ) : null} + + {projectLinks.length === 0 && !showAddLink ? ( +

No links yet — add one.

+ ) : null} + +
+ {projectLinks.map((link, i) => ( +
+ {editingLinkIdx === i ? ( + <> + setEditingLinkLabel(e.target.value)} + className="h-6 w-24 shrink-0 text-xs" + /> + setEditingLinkUrl(e.target.value)} + className="h-6 min-w-0 flex-1 text-xs" + onKeyDown={(e) => { + if (e.key === "Enter") commitEditLink(i) + if (e.key === "Escape") setEditingLinkIdx(null) + }} + autoFocus + /> + + + + ) : ( + <> + + + + + {link.label || "Link"} + + + {link.url} + +
+ + {deleteLinkIdx === i ? ( +
+ + +
+ ) : ( + + )} +
+ + )} +
+ ))}
- ) : null} +
{/* Footer actions */} From 012fd11b439ae4a193291beed2b56503929a3d79 Mon Sep 17 00:00:00 2001 From: Roman Mazuryk Date: Mon, 29 Jun 2026 14:11:44 +0200 Subject: [PATCH 5/5] =?UTF-8?q?feat(projects):=20=C2=A74-6=20=E2=80=94=20s?= =?UTF-8?q?ticky=20footer,=20keyboard=20shortcuts,=20layout=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §4 — Modal footer: - Sticky bottom row: Delete project (muted red text btn, AlertDialog confirm) on the left; Cancel (ghost) + Save (primary) on the right - Save disabled when form is pristine (edit mode) or invalid - formError moved inline under Duration for date validation §5 — Layout polish: - DialogContent: flex-col, p-0, max-w-[480px], max-h-[90vh] - Header: border-b, px-5 pt-5 pb-4 - Body: overflow-y-auto, space-y-4, uniform label sizing - Milestones field (create-only) has consistent mt-1.5 spacing §6 — Keyboard: - Cmd/Ctrl+Enter triggers Save when valid and dirty Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 17 +++++ components/modules/projects-module.tsx | 93 ++++++++++++++++++++------ 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac7fbf..52152cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) --- +## [Unreleased] - 2026-06-29 — Edit Project modal identity strip (session 1) + +### Changed +- Edit Project modal stripped to identity-only fields: Title, Objective, Duration, Color, Status +- Weekly Outcome field removed from modal (field retained on type for session-2 migration) +- Links section removed from modal; moved to detail Sheet as inline-editable block +- Date pickers: native `` → shadcn `Calendar + Popover`, format `DD MMM YYYY`, end-date-after-start validation +- Color picker: hex input + native color input → row of 8 preset swatches with ring-on-active selection +- Status: `Select` dropdown → `ToggleGroup` segmented control (Active / Paused / Parked / Completed) +- Modal footer: sticky `Delete project` (muted red text) · `Cancel` · `Save`; Save disabled when pristine or invalid; Escape closes; Cmd/Ctrl+Enter saves + +### Added +- ADR-007 in `docs/DECISIONS_LOG.md`: project data model decision (milestones-only, sprint as separate object) +- Links block on detail Sheet: `+ Add`, inline edit, inline delete confirm, URL tooltip on hover + +--- + ## [Unreleased] - 2026-06-26 — Schedule block editor improvements ### Added diff --git a/components/modules/projects-module.tsx b/components/modules/projects-module.tsx index c947b6a..24fbc0c 100644 --- a/components/modules/projects-module.tsx +++ b/components/modules/projects-module.tsx @@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog" import { Calendar } from "@/components/ui/calendar" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" @@ -139,6 +140,7 @@ export function ProjectsModule() { const [formError, setFormError] = useState(null) const [startDateOpen, setStartDateOpen] = useState(false) const [endDateOpen, setEndDateOpen] = useState(false) + const [formSnapshot, setFormSnapshot] = useState<{ title: string; objective: string; status: string; startDate: string; endDate: string; color: string } | null>(null) // Module state const [view, setView] = useState<"list" | "timeline">("list") @@ -178,19 +180,23 @@ export function ProjectsModule() { setEndDate(defaultWeekRange.end) setMilestones("") setFormError(null) + setFormSnapshot(null) setOpen(true) } function openEditDialog(project: Project) { + const s = getProjectStatus(project) + const c = normalizeProjectColor(project.color) setEditingId(project.id) setTitle(project.title) setObjective(project.objective) - setStatus(getProjectStatus(project)) - setColor(normalizeProjectColor(project.color)) + setStatus(s) + setColor(c) setStartDate(project.weekStartISO) setEndDate(project.weekEndISO) setMilestones(project.milestones.map((m) => m.title).join(", ")) setFormError(null) + setFormSnapshot({ title: project.title, objective: project.objective, status: s, startDate: project.weekStartISO, endDate: project.weekEndISO, color: c }) setOpen(true) } @@ -280,18 +286,28 @@ export function ProjectsModule() { New Project - - - {editingId ? "Edit Project" : "Create Project"} + { + if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { + e.preventDefault() + const isDirty = !formSnapshot || title !== formSnapshot.title || objective !== formSnapshot.objective || status !== formSnapshot.status || startDate !== formSnapshot.startDate || endDate !== formSnapshot.endDate || color !== formSnapshot.color + const isValid = !!title.trim() && (startDate <= endDate) + if (isValid && (!editingId || isDirty)) saveProject() + } + }} + > + + {editingId ? "Edit Project" : "Create Project"} -
+
- - setTitle(e.target.value)} /> + + setTitle(e.target.value)} className="mt-1.5" />
- - setObjective(e.target.value)} /> + + setObjective(e.target.value)} className="mt-1.5" />
@@ -348,6 +364,9 @@ export function ProjectsModule() {
+ {formError ? ( +

{formError}

+ ) : null}
@@ -372,23 +391,59 @@ export function ProjectsModule() {
{!editingId ? (
- + setMilestones(e.target.value)} placeholder="Design, Build page, Deploy" + className="mt-1.5" />
) : null} - {formError ? ( -
- {formError} -
- ) : null} - +
+ {/* §4 — sticky footer */} +
+ {editingId ? ( + + + + + + + Delete "{title}"? + This cannot be undone. + + + Cancel + { deleteProject(editingId); setOpen(false) }} + > + Delete + + + + + ) : ( + + )} +
+ + +