diff --git a/frontend/src/components/KanbanView.jsx b/frontend/src/components/KanbanView.jsx
index 9d63c29..6474978 100644
--- a/frontend/src/components/KanbanView.jsx
+++ b/frontend/src/components/KanbanView.jsx
@@ -8,6 +8,7 @@ import {
} from '../utils/api'
import { formatTimeWithTotal } from '../utils/format'
import TaskMenu from './TaskMenu'
+import TaskForm from './TaskForm'
const STATUSES = [
{ key: 'backlog', label: 'Backlog', color: 'border-gray-600' },
@@ -150,20 +151,18 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
function KanbanColumn({ status, tasks, allTasks, projectId, onUpdate, onDrop, onDragOver }) {
const [showAddTask, setShowAddTask] = useState(false)
- const [newTaskTitle, setNewTaskTitle] = useState('')
-
- const handleAddTask = async (e) => {
- e.preventDefault()
- if (!newTaskTitle.trim()) return
+ const handleAddTask = async (taskData) => {
try {
await createTask({
project_id: parseInt(projectId),
parent_task_id: null,
- title: newTaskTitle,
- status: status.key
+ title: taskData.title,
+ status: status.key,
+ tags: taskData.tags,
+ estimated_minutes: taskData.estimated_minutes,
+ flag_color: taskData.flag_color
})
- setNewTaskTitle('')
setShowAddTask(false)
onUpdate()
} catch (err) {
@@ -192,31 +191,11 @@ function KanbanColumn({ status, tasks, allTasks, projectId, onUpdate, onDrop, on
{showAddTask && (
- Time Estimate (minutes)
+ Time Estimate
-
diff --git a/frontend/src/components/TreeView.jsx b/frontend/src/components/TreeView.jsx
index ac1ed90..f9c9de1 100644
--- a/frontend/src/components/TreeView.jsx
+++ b/frontend/src/components/TreeView.jsx
@@ -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 && (
-
+ setShowAddSubtask(false)}
+ submitLabel="Add Subtask"
+ />
)}
@@ -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 && (
-
+ setShowAddRoot(false)}
+ submitLabel="Add Task"
+ />
)}
diff --git a/frontend/src/utils/format.js b/frontend/src/utils/format.js
index 3e073f6..6139222 100644
--- a/frontend/src/utils/format.js
+++ b/frontend/src/utils/format.js
@@ -1,4 +1,4 @@
-// Format minutes into display string
+// Format minutes into display string (e.g., "1h 30m" or "45m")
export function formatTime(minutes) {
if (!minutes || minutes === 0) return null;
@@ -6,8 +6,14 @@ export function formatTime(minutes) {
return `${minutes}m`;
}
- const hours = minutes / 60;
- return `${hours.toFixed(1)}h`;
+ const hours = Math.floor(minutes / 60);
+ const mins = minutes % 60;
+
+ if (mins === 0) {
+ return `${hours}h`;
+ }
+
+ return `${hours}h ${mins}m`;
}
// Format tags as comma-separated string
@@ -16,58 +22,63 @@ export function formatTags(tags) {
return tags.join(', ');
}
-// Recursively calculate total time estimate including all subtasks
-export function calculateTotalTime(task) {
- let total = task.estimated_minutes || 0;
+// Calculate sum of all LEAF descendant estimates (hierarchical structure)
+export function calculateLeafTime(task) {
+ // If no subtasks, this is a leaf - return its own estimate
+ if (!task.subtasks || task.subtasks.length === 0) {
+ return task.estimated_minutes || 0;
+ }
- if (task.subtasks && task.subtasks.length > 0) {
- for (const subtask of task.subtasks) {
- total += calculateTotalTime(subtask);
- }
+ // Has subtasks, so sum up all leaf descendants
+ let total = 0;
+ for (const subtask of task.subtasks) {
+ total += calculateLeafTime(subtask);
}
return total;
}
-// Calculate total time for a task from a flat list of all tasks
-export function calculateTotalTimeFlat(task, allTasks) {
- let total = task.estimated_minutes || 0;
-
- // Find all direct children
+// Calculate sum of all LEAF descendant estimates (flat task list)
+export function calculateLeafTimeFlat(task, allTasks) {
+ // Find direct children
const children = allTasks.filter(t => t.parent_task_id === task.id);
- // Recursively add their times
+ // If no children, this is a leaf - return its own estimate
+ if (children.length === 0) {
+ return task.estimated_minutes || 0;
+ }
+
+ // Has children, so sum up all leaf descendants
+ let total = 0;
for (const child of children) {
- total += calculateTotalTimeFlat(child, allTasks);
+ total += calculateLeafTimeFlat(child, allTasks);
}
return total;
}
-// Format time display showing own estimate and total if different
+// Format time display based on leaf calculation logic
export function formatTimeWithTotal(task, allTasks = null) {
- const ownTime = task.estimated_minutes || 0;
+ // Check if task has subtasks
+ const hasSubtasks = allTasks
+ ? allTasks.some(t => t.parent_task_id === task.id)
+ : (task.subtasks && task.subtasks.length > 0);
- // If we have a flat task list, use that to calculate total
- const totalTime = allTasks
- ? calculateTotalTimeFlat(task, allTasks)
- : calculateTotalTime(task);
-
- const subtaskTime = totalTime - ownTime;
-
- // No time estimates at all
- if (totalTime === 0) return null;
-
- // Only own estimate, no subtasks with time
- if (subtaskTime === 0) {
- return formatTime(ownTime);
+ // Leaf task: use own estimate
+ if (!hasSubtasks) {
+ return formatTime(task.estimated_minutes);
}
- // Only subtask estimates, no own estimate
- if (ownTime === 0) {
- return `(${formatTime(totalTime)} from subtasks)`;
+ // Parent task: calculate sum of leaf descendants
+ const leafTotal = allTasks
+ ? calculateLeafTimeFlat(task, allTasks)
+ : calculateLeafTime(task);
+
+ // If no leaf estimates exist, fall back to own estimate
+ if (leafTotal === 0) {
+ return formatTime(task.estimated_minutes);
}
- // Both own and subtask estimates
- return `${formatTime(ownTime)} (${formatTime(totalTime)} total)`;
+ // Show leaf total
+ return formatTime(leafTotal);
}