feat: task time estimates and velocity chart on dashboard
- Added estimatedHours column to tasks schema - Backend: create/update support for estimatedHours - New /api/tasks/stats/velocity endpoint: daily completions, weekly velocity, estimate totals - Dashboard: velocity chart with 7-day bar chart, this week count, avg/week, estimate summary - TaskDetailPanel: estimated hours input field - CreateTaskModal: estimated hours in advanced options - TaskCard, KanbanBoard, TaskPage: estimate badge display
This commit is contained in:
@@ -154,6 +154,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
taskNumber: nextNumber,
|
||||
projectId: body.projectId || null,
|
||||
dueDate: body.dueDate ? new Date(body.dueDate) : null,
|
||||
estimatedHours: body.estimatedHours ?? null,
|
||||
tags: body.tags || [],
|
||||
subtasks: [],
|
||||
progressNotes: [],
|
||||
@@ -194,11 +195,70 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
),
|
||||
projectId: t.Optional(t.Union([t.String(), t.Null()])),
|
||||
dueDate: t.Optional(t.Union([t.String(), t.Null()])),
|
||||
estimatedHours: t.Optional(t.Union([t.Number(), t.Null()])),
|
||||
tags: t.Optional(t.Array(t.String())),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
// GET stats - velocity and estimates - requires auth
|
||||
.get("/stats/velocity", async ({ request, headers }) => {
|
||||
await requireSessionOrBearer(request, headers);
|
||||
|
||||
const allTasks = await db.select().from(tasks);
|
||||
|
||||
// Build daily completion counts for last 14 days
|
||||
const dailyCompletions: { date: string; count: number }[] = [];
|
||||
for (let i = 13; i >= 0; i--) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - i);
|
||||
const dateStr = d.toISOString().split("T")[0];
|
||||
const count = allTasks.filter(t => {
|
||||
if (!t.completedAt) return false;
|
||||
const completedDate = new Date(t.completedAt).toISOString().split("T")[0];
|
||||
return completedDate === dateStr;
|
||||
}).length;
|
||||
dailyCompletions.push({ date: dateStr, count });
|
||||
}
|
||||
|
||||
// Estimate totals
|
||||
const activeAndQueued = allTasks.filter(t => t.status === "active" || t.status === "queued");
|
||||
const totalEstimated = activeAndQueued.reduce((sum, t) => sum + (t.estimatedHours || 0), 0);
|
||||
const estimatedCount = activeAndQueued.filter(t => t.estimatedHours).length;
|
||||
const unestimatedCount = activeAndQueued.length - estimatedCount;
|
||||
|
||||
// Completed this week (Mon-Sun)
|
||||
const now = new Date();
|
||||
const dayOfWeek = now.getDay();
|
||||
const mondayOffset = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
||||
const monday = new Date(now);
|
||||
monday.setDate(monday.getDate() - mondayOffset);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
const completedThisWeek = allTasks.filter(t => {
|
||||
if (!t.completedAt) return false;
|
||||
return new Date(t.completedAt) >= monday;
|
||||
}).length;
|
||||
|
||||
// Average tasks completed per week (last 4 weeks)
|
||||
const fourWeeksAgo = new Date();
|
||||
fourWeeksAgo.setDate(fourWeeksAgo.getDate() - 28);
|
||||
const completedLast4Weeks = allTasks.filter(t => {
|
||||
if (!t.completedAt) return false;
|
||||
return new Date(t.completedAt) >= fourWeeksAgo;
|
||||
}).length;
|
||||
const avgPerWeek = Math.round((completedLast4Weeks / 4) * 10) / 10;
|
||||
|
||||
return {
|
||||
dailyCompletions,
|
||||
completedThisWeek,
|
||||
avgPerWeek,
|
||||
totalEstimatedHours: totalEstimated,
|
||||
estimatedTaskCount: estimatedCount,
|
||||
unestimatedTaskCount: unestimatedCount,
|
||||
};
|
||||
})
|
||||
|
||||
// GET single task by ID or number - requires session or bearer auth
|
||||
.get(
|
||||
"/:id",
|
||||
@@ -242,6 +302,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
if (body.assigneeName !== undefined) updates.assigneeName = body.assigneeName;
|
||||
if (body.projectId !== undefined) updates.projectId = body.projectId;
|
||||
if (body.dueDate !== undefined) updates.dueDate = body.dueDate ? new Date(body.dueDate) : null;
|
||||
if (body.estimatedHours !== undefined) updates.estimatedHours = body.estimatedHours;
|
||||
if (body.tags !== undefined) updates.tags = body.tags;
|
||||
if (body.subtasks !== undefined) updates.subtasks = body.subtasks;
|
||||
if (body.progressNotes !== undefined) updates.progressNotes = body.progressNotes;
|
||||
@@ -273,6 +334,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
assigneeName: t.Optional(t.Union([t.String(), t.Null()])),
|
||||
projectId: t.Optional(t.Union([t.String(), t.Null()])),
|
||||
dueDate: t.Optional(t.Union([t.String(), t.Null()])),
|
||||
estimatedHours: t.Optional(t.Union([t.Number(), t.Null()])),
|
||||
tags: t.Optional(t.Array(t.String())),
|
||||
subtasks: t.Optional(t.Array(t.Object({
|
||||
id: t.String(),
|
||||
|
||||
Reference in New Issue
Block a user