Main branch Resync on w/ gitea. v0.1.6 #1
@@ -6,7 +6,7 @@ import {
|
|||||||
updateTask,
|
updateTask,
|
||||||
deleteTask
|
deleteTask
|
||||||
} from '../utils/api'
|
} from '../utils/api'
|
||||||
import { formatTime } from '../utils/format'
|
import { formatTimeWithTotal } from '../utils/format'
|
||||||
import TaskMenu from './TaskMenu'
|
import TaskMenu from './TaskMenu'
|
||||||
|
|
||||||
const STATUSES = [
|
const STATUSES = [
|
||||||
@@ -106,13 +106,13 @@ function TaskCard({ task, allTasks, onUpdate, onDragStart }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Metadata row */}
|
{/* Metadata row */}
|
||||||
{(task.estimated_minutes || (task.tags && task.tags.length > 0)) && (
|
{(formatTimeWithTotal(task, allTasks) || (task.tags && task.tags.length > 0)) && (
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
{/* Time estimate */}
|
{/* Time estimate */}
|
||||||
{task.estimated_minutes && (
|
{formatTimeWithTotal(task, allTasks) && (
|
||||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||||
<Clock size={11} />
|
<Clock size={11} />
|
||||||
<span>{formatTime(task.estimated_minutes)}</span>
|
<span>{formatTimeWithTotal(task, allTasks)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
import { MoreVertical, Clock, Tag, Flag, Edit2, Trash2, X, Check } from 'lucide-react'
|
import { MoreVertical, Clock, Tag, Flag, Edit2, Trash2, X, Check, ListTodo } from 'lucide-react'
|
||||||
import { updateTask } from '../utils/api'
|
import { updateTask } from '../utils/api'
|
||||||
|
|
||||||
const FLAG_COLORS = [
|
const FLAG_COLORS = [
|
||||||
@@ -12,11 +12,19 @@ const FLAG_COLORS = [
|
|||||||
{ name: 'pink', color: 'bg-pink-500' }
|
{ name: 'pink', color: 'bg-pink-500' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const STATUSES = [
|
||||||
|
{ key: 'backlog', label: 'Backlog', color: 'text-gray-400' },
|
||||||
|
{ key: 'in_progress', label: 'In Progress', color: 'text-blue-400' },
|
||||||
|
{ key: 'blocked', label: 'Blocked', color: 'text-red-400' },
|
||||||
|
{ key: 'done', label: 'Done', color: 'text-green-400' }
|
||||||
|
]
|
||||||
|
|
||||||
function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
|
function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [showTimeEdit, setShowTimeEdit] = useState(false)
|
const [showTimeEdit, setShowTimeEdit] = useState(false)
|
||||||
const [showTagsEdit, setShowTagsEdit] = useState(false)
|
const [showTagsEdit, setShowTagsEdit] = useState(false)
|
||||||
const [showFlagEdit, setShowFlagEdit] = useState(false)
|
const [showFlagEdit, setShowFlagEdit] = useState(false)
|
||||||
|
const [showStatusEdit, setShowStatusEdit] = useState(false)
|
||||||
const [editTime, setEditTime] = useState(task.estimated_minutes || '')
|
const [editTime, setEditTime] = useState(task.estimated_minutes || '')
|
||||||
const [editTags, setEditTags] = useState(task.tags ? task.tags.join(', ') : '')
|
const [editTags, setEditTags] = useState(task.tags ? task.tags.join(', ') : '')
|
||||||
const menuRef = useRef(null)
|
const menuRef = useRef(null)
|
||||||
@@ -28,6 +36,7 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
|
|||||||
setShowTimeEdit(false)
|
setShowTimeEdit(false)
|
||||||
setShowTagsEdit(false)
|
setShowTagsEdit(false)
|
||||||
setShowFlagEdit(false)
|
setShowFlagEdit(false)
|
||||||
|
setShowStatusEdit(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +94,17 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpdateStatus = async (newStatus) => {
|
||||||
|
try {
|
||||||
|
await updateTask(task.id, { status: newStatus })
|
||||||
|
setShowStatusEdit(false)
|
||||||
|
setIsOpen(false)
|
||||||
|
onUpdate()
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Error: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={menuRef}>
|
<div className="relative" ref={menuRef}>
|
||||||
<button
|
<button
|
||||||
@@ -225,6 +245,42 @@ function TaskMenu({ task, onUpdate, onDelete, onEdit }) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Status Change */}
|
||||||
|
{showStatusEdit ? (
|
||||||
|
<div className="p-3 border-b border-cyber-orange/20">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<ListTodo size={14} className="text-cyber-orange" />
|
||||||
|
<span className="text-sm text-gray-300">Change Status</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{STATUSES.map(({ key, label, color }) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => handleUpdateStatus(key)}
|
||||||
|
className={`w-full text-left px-2 py-1.5 rounded text-sm ${
|
||||||
|
task.status === key
|
||||||
|
? 'bg-cyber-orange/20 border border-cyber-orange/40'
|
||||||
|
: 'hover:bg-cyber-darker border border-transparent'
|
||||||
|
} ${color} transition-all`}
|
||||||
|
>
|
||||||
|
{label} {task.status === key && '✓'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setShowStatusEdit(true)
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-cyber-darker text-gray-300 text-sm"
|
||||||
|
>
|
||||||
|
<ListTodo size={14} />
|
||||||
|
<span>Change Status</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Edit Title */}
|
{/* Edit Title */}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
updateTask,
|
updateTask,
|
||||||
deleteTask
|
deleteTask
|
||||||
} from '../utils/api'
|
} from '../utils/api'
|
||||||
import { formatTime } from '../utils/format'
|
import { formatTimeWithTotal } from '../utils/format'
|
||||||
import TaskMenu from './TaskMenu'
|
import TaskMenu from './TaskMenu'
|
||||||
|
|
||||||
const STATUS_COLORS = {
|
const STATUS_COLORS = {
|
||||||
@@ -163,13 +163,13 @@ function TaskNode({ task, projectId, onUpdate, level = 0 }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metadata row */}
|
{/* Metadata row */}
|
||||||
{(task.estimated_minutes || (task.tags && task.tags.length > 0)) && (
|
{(formatTimeWithTotal(task) || (task.tags && task.tags.length > 0)) && (
|
||||||
<div className="flex items-center gap-3 mt-1">
|
<div className="flex items-center gap-3 mt-1">
|
||||||
{/* Time estimate */}
|
{/* Time estimate */}
|
||||||
{task.estimated_minutes && (
|
{formatTimeWithTotal(task) && (
|
||||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||||
<Clock size={12} />
|
<Clock size={12} />
|
||||||
<span>{formatTime(task.estimated_minutes)}</span>
|
<span>{formatTimeWithTotal(task)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,59 @@ export function formatTags(tags) {
|
|||||||
if (!tags || tags.length === 0) return null;
|
if (!tags || tags.length === 0) return null;
|
||||||
return tags.join(', ');
|
return tags.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively calculate total time estimate including all subtasks
|
||||||
|
export function calculateTotalTime(task) {
|
||||||
|
let total = task.estimated_minutes || 0;
|
||||||
|
|
||||||
|
if (task.subtasks && task.subtasks.length > 0) {
|
||||||
|
for (const subtask of task.subtasks) {
|
||||||
|
total += calculateTotalTime(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
|
||||||
|
const children = allTasks.filter(t => t.parent_task_id === task.id);
|
||||||
|
|
||||||
|
// Recursively add their times
|
||||||
|
for (const child of children) {
|
||||||
|
total += calculateTotalTimeFlat(child, allTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format time display showing own estimate and total if different
|
||||||
|
export function formatTimeWithTotal(task, allTasks = null) {
|
||||||
|
const ownTime = task.estimated_minutes || 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only subtask estimates, no own estimate
|
||||||
|
if (ownTime === 0) {
|
||||||
|
return `(${formatTime(totalTime)} from subtasks)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both own and subtask estimates
|
||||||
|
return `${formatTime(ownTime)} (${formatTime(totalTime)} total)`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user