mission-control/lib/data/tasks.ts
OpenClaw Bot 762c59500e Mission Control Phase 2: Transform Tasks page to overview
- Replaced kanban with task overview/summary view
- Added task stats cards (total, in progress, high priority, overdue)
- Added recent activity sections (updated, completed, high priority)
- Added quick action links to gantt-board
- Created lib/data/tasks.ts with data fetching functions
- Removed file-based storage (taskDb.ts, api/tasks/route.ts)
- Connected to gantt-board Supabase for real data
2026-02-21 22:48:15 -06:00

285 lines
8.0 KiB
TypeScript

import { getServiceSupabase } from "@/lib/supabase/client";
export interface Task {
id: string;
title: string;
description?: string;
type: "idea" | "task" | "bug" | "research" | "plan";
status: "open" | "todo" | "blocked" | "in-progress" | "review" | "validate" | "archived" | "canceled" | "done";
priority: "low" | "medium" | "high" | "urgent";
projectId: string;
sprintId?: string;
createdAt: string;
updatedAt: string;
createdById?: string;
createdByName?: string;
createdByAvatarUrl?: string;
updatedById?: string;
updatedByName?: string;
updatedByAvatarUrl?: string;
assigneeId?: string;
assigneeName?: string;
assigneeEmail?: string;
assigneeAvatarUrl?: string;
dueDate?: string;
comments: unknown[];
tags: string[];
attachments: unknown[];
}
const TASK_STATUSES: Task["status"][] = ["open", "todo", "blocked", "in-progress", "review", "validate", "archived", "canceled", "done"];
function isTaskStatus(value: unknown): value is Task["status"] {
return typeof value === "string" && TASK_STATUSES.includes(value as Task["status"]);
}
function toNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
}
function mapTaskRow(row: Record<string, unknown>): Task {
const fallbackDate = new Date().toISOString();
return {
id: String(row.id ?? ""),
title: toNonEmptyString(row.title) ?? "",
description: toNonEmptyString(row.description),
type: (toNonEmptyString(row.type) as Task["type"]) ?? "task",
status: isTaskStatus(row.status) ? row.status : "todo",
priority: (toNonEmptyString(row.priority) as Task["priority"]) ?? "medium",
projectId: String(row.project_id ?? ""),
sprintId: toNonEmptyString(row.sprint_id),
createdAt: toNonEmptyString(row.created_at) ?? fallbackDate,
updatedAt: toNonEmptyString(row.updated_at) ?? fallbackDate,
createdById: toNonEmptyString(row.created_by_id),
createdByName: toNonEmptyString(row.created_by_name),
createdByAvatarUrl: toNonEmptyString(row.created_by_avatar_url),
updatedById: toNonEmptyString(row.updated_by_id),
updatedByName: toNonEmptyString(row.updated_by_name),
updatedByAvatarUrl: toNonEmptyString(row.updated_by_avatar_url),
assigneeId: toNonEmptyString(row.assignee_id),
assigneeName: toNonEmptyString(row.assignee_name),
assigneeEmail: toNonEmptyString(row.assignee_email),
assigneeAvatarUrl: toNonEmptyString(row.assignee_avatar_url),
dueDate: toNonEmptyString(row.due_date),
comments: Array.isArray(row.comments) ? row.comments : [],
tags: Array.isArray(row.tags) ? row.tags.filter((tag): tag is string => typeof tag === "string") : [],
attachments: Array.isArray(row.attachments) ? row.attachments : [],
};
}
/**
* Fetch all tasks from Supabase
*/
export async function fetchAllTasks(): Promise<Task[]> {
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from("tasks")
.select("*")
.order("created_at", { ascending: true });
if (error) {
console.error("Error fetching tasks:", error);
throw new Error(`Failed to fetch tasks: ${error.message}`);
}
return (data || []).map((row) => mapTaskRow(row as Record<string, unknown>));
}
/**
* Fetch active tasks (status != 'done')
*/
export async function fetchActiveTasks(): Promise<Task[]> {
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from("tasks")
.select("*")
.neq("status", "done")
.order("created_at", { ascending: true });
if (error) {
console.error("Error fetching active tasks:", error);
throw new Error(`Failed to fetch active tasks: ${error.message}`);
}
return (data || []).map((row) => mapTaskRow(row as Record<string, unknown>));
}
/**
* Count active tasks
*/
export async function countActiveTasks(): Promise<number> {
const supabase = getServiceSupabase();
const { count, error } = await supabase
.from("tasks")
.select("*", { count: "exact", head: true })
.neq("status", "done");
if (error) {
console.error("Error counting active tasks:", error);
return 0;
}
return count || 0;
}
/**
* Count total tasks
*/
export async function countTotalTasks(): Promise<number> {
const supabase = getServiceSupabase();
const { count, error } = await supabase
.from("tasks")
.select("*", { count: "exact", head: true });
if (error) {
console.error("Error counting total tasks:", error);
return 0;
}
return count || 0;
}
/**
* Get task counts by status
*/
export interface TaskStatusCounts {
open: number;
inProgress: number;
review: number;
done: number;
total: number;
}
export async function getTaskStatusCounts(): Promise<TaskStatusCounts> {
const supabase = getServiceSupabase();
// Get all tasks and count by status
const { data, error } = await supabase
.from("tasks")
.select("status");
if (error) {
console.error("Error fetching task status counts:", error);
return { open: 0, inProgress: 0, review: 0, done: 0, total: 0 };
}
const counts = { open: 0, inProgress: 0, review: 0, done: 0, total: 0 };
for (const row of data || []) {
counts.total++;
const status = row.status;
if (status === "open" || status === "todo" || status === "blocked") {
counts.open++;
} else if (status === "in-progress") {
counts.inProgress++;
} else if (status === "review" || status === "validate") {
counts.review++;
} else if (status === "done") {
counts.done++;
}
}
return counts;
}
/**
* Count high priority tasks (high or urgent priority, not done)
*/
export async function countHighPriorityTasks(): Promise<number> {
const supabase = getServiceSupabase();
const { count, error } = await supabase
.from("tasks")
.select("*", { count: "exact", head: true })
.in("priority", ["high", "urgent"])
.neq("status", "done");
if (error) {
console.error("Error counting high priority tasks:", error);
return 0;
}
return count || 0;
}
/**
* Count overdue tasks (due date in the past, not done)
*/
export async function countOverdueTasks(): Promise<number> {
const supabase = getServiceSupabase();
const today = new Date().toISOString().split("T")[0];
const { count, error } = await supabase
.from("tasks")
.select("*", { count: "exact", head: true })
.lt("due_date", today)
.neq("status", "done")
.not("due_date", "is", null);
if (error) {
console.error("Error counting overdue tasks:", error);
return 0;
}
return count || 0;
}
/**
* Fetch recently updated tasks (last 5)
*/
export async function fetchRecentlyUpdatedTasks(limit = 5): Promise<Task[]> {
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from("tasks")
.select("*")
.order("updated_at", { ascending: false })
.limit(limit);
if (error) {
console.error("Error fetching recently updated tasks:", error);
return [];
}
return (data || []).map((row) => mapTaskRow(row as Record<string, unknown>));
}
/**
* Fetch recently completed tasks (last 5)
*/
export async function fetchRecentlyCompletedTasks(limit = 5): Promise<Task[]> {
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from("tasks")
.select("*")
.eq("status", "done")
.order("updated_at", { ascending: false })
.limit(limit);
if (error) {
console.error("Error fetching recently completed tasks:", error);
return [];
}
return (data || []).map((row) => mapTaskRow(row as Record<string, unknown>));
}
/**
* Fetch high priority open tasks (top 5)
*/
export async function fetchHighPriorityOpenTasks(limit = 5): Promise<Task[]> {
const supabase = getServiceSupabase();
const { data, error } = await supabase
.from("tasks")
.select("*")
.in("priority", ["high", "urgent"])
.neq("status", "done")
.order("updated_at", { ascending: false })
.limit(limit);
if (error) {
console.error("Error fetching high priority open tasks:", error);
return [];
}
return (data || []).map((row) => mapTaskRow(row as Record<string, unknown>));
}