Add enhanced task creation forms and leaf-based time calculation

Features:

1. Enhanced Task Creation Forms:
   - New TaskForm component with all metadata fields
   - Title, tags (comma-separated), time estimate (hours + minutes), flag color
   - Used in TreeView (root tasks and subtasks) and KanbanView (all columns)
   - Replace simple title-only inputs with full metadata forms

2. Time Format Changes:
   - Display: "1h 30m" instead of "1.5h"
   - Input: Separate hours and minutes fields
   - Storage: Still integer minutes in backend
   - Updated formatTime() utility
   - Updated TaskMenu time editor with hours/minutes inputs

3. Leaf-Based Time Calculation:
   - Leaf tasks (no subtasks): Show user-entered estimate
   - Parent tasks (has subtasks): Show sum of all descendant LEAF tasks
   - Exception: Parent with no leaf estimates shows own estimate as fallback
   - New functions: calculateLeafTime(), calculateLeafTimeFlat()
   - Replaces old aggregation that summed all tasks including parents

This allows accurate project planning where parent estimates are calculated from leaf tasks,
preventing double-counting when both parent and children have estimates.
This commit is contained in:
Claude
2025-11-20 09:37:16 +00:00
parent 3f309163b6
commit c9555737d8
5 changed files with 256 additions and 143 deletions

View File

@@ -16,6 +16,7 @@ import {
} from '../utils/api'
import { formatTimeWithTotal } from '../utils/format'
import TaskMenu from './TaskMenu'
import TaskForm from './TaskForm'
const STATUS_COLORS = {
backlog: 'text-gray-400',
@@ -47,7 +48,6 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
const [editTitle, setEditTitle] = useState(task.title)
const [editStatus, setEditStatus] = useState(task.status)
const [showAddSubtask, setShowAddSubtask] = useState(false)
const [newSubtaskTitle, setNewSubtaskTitle] = useState('')
const hasSubtasks = task.subtasks && task.subtasks.length > 0
@@ -74,18 +74,17 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
}
}
const handleAddSubtask = async (e) => {
e.preventDefault()
if (!newSubtaskTitle.trim()) return
const handleAddSubtask = async (taskData) => {
try {
await createTask({
project_id: parseInt(projectId),
parent_task_id: task.id,
title: newSubtaskTitle,
status: 'backlog'
title: taskData.title,
status: 'backlog',
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,
flag_color: taskData.flag_color
})
setNewSubtaskTitle('')
setShowAddSubtask(false)
setIsExpanded(true)
onUpdate()
@@ -213,29 +212,11 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
{/* Add Subtask Form */}
{showAddSubtask && (
<div style={{ marginLeft: `${level * 1.5}rem` }} className="mt-2">
<form onSubmit={handleAddSubtask} className="flex gap-2">
<input
type="text"
value={newSubtaskTitle}
onChange={(e) => setNewSubtaskTitle(e.target.value)}
placeholder="New subtask title..."
className="flex-1 px-3 py-2 bg-cyber-darker border border-cyber-orange/50 rounded text-gray-100 text-sm focus:outline-none focus:border-cyber-orange"
autoFocus
/>
<button
type="submit"
className="px-3 py-2 bg-cyber-orange text-cyber-darkest rounded hover:bg-cyber-orange-bright text-sm font-semibold"
>
Add
</button>
<button
type="button"
onClick={() => setShowAddSubtask(false)}
className="px-3 py-2 text-gray-400 hover:text-gray-200 text-sm"
>
Cancel
</button>
</form>
<TaskForm
onSubmit={handleAddSubtask}
onCancel={() => setShowAddSubtask(false)}
submitLabel="Add Subtask"
/>
</div>
)}
@@ -262,7 +243,6 @@ function TreeView({ projectId }) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [showAddRoot, setShowAddRoot] = useState(false)
const [newTaskTitle, setNewTaskTitle] = useState('')
useEffect(() => {
loadTasks()
@@ -280,18 +260,17 @@ function TreeView({ projectId }) {
}
}
const handleAddRootTask = async (e) => {
e.preventDefault()
if (!newTaskTitle.trim()) return
const handleAddRootTask = async (taskData) => {
try {
await createTask({
project_id: parseInt(projectId),
parent_task_id: null,
title: newTaskTitle,
status: 'backlog'
title: taskData.title,
status: 'backlog',
tags: taskData.tags,
estimated_minutes: taskData.estimated_minutes,
flag_color: taskData.flag_color
})
setNewTaskTitle('')
setShowAddRoot(false)
loadTasks()
} catch (err) {
@@ -322,29 +301,11 @@ function TreeView({ projectId }) {
{showAddRoot && (
<div className="mb-4">
<form onSubmit={handleAddRootTask} className="flex gap-2">
<input
type="text"
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
placeholder="New task title..."
className="flex-1 px-3 py-2 bg-cyber-darker border border-cyber-orange/50 rounded text-gray-100 focus:outline-none focus:border-cyber-orange"
autoFocus
/>
<button
type="submit"
className="px-4 py-2 bg-cyber-orange text-cyber-darkest rounded hover:bg-cyber-orange-bright font-semibold"
>
Add
</button>
<button
type="button"
onClick={() => setShowAddRoot(false)}
className="px-4 py-2 text-gray-400 hover:text-gray-200"
>
Cancel
</button>
</form>
<TaskForm
onSubmit={handleAddRootTask}
onCancel={() => setShowAddRoot(false)}
submitLabel="Add Task"
/>
</div>
)}