- 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
285 lines
8.0 KiB
TypeScript
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>));
|
|
}
|