import { getServiceSupabase } from "@/lib/supabase/client"; import { Task } from "./tasks"; export interface Project { id: string; name: string; description?: string; color: string; createdAt: string; } export interface Sprint { id: string; name: string; status: "planning" | "active" | "completed" | "cancelled"; startDate: string; endDate: string; projectId: string; goal?: string; } export interface ProjectStats { project: Project; totalTasks: number; completedTasks: number; inProgressTasks: number; urgentTasks: number; highPriorityTasks: number; progress: number; recentTasks: Task[]; } function toNonEmptyString(value: unknown): string | undefined { return typeof value === "string" && value.trim().length > 0 ? value : undefined; } function mapProjectRow(row: Record): Project { return { id: String(row.id ?? ""), name: toNonEmptyString(row.name) ?? "Untitled Project", description: toNonEmptyString(row.description), color: toNonEmptyString(row.color) ?? "#3b82f6", createdAt: toNonEmptyString(row.created_at) ?? new Date().toISOString(), }; } /** * Fetch all projects from Supabase */ export async function fetchAllProjects(): Promise { const supabase = getServiceSupabase(); const { data, error } = await supabase .from("projects") .select("*") .order("created_at", { ascending: true }); if (error) { console.error("Error fetching projects:", error); throw new Error(`Failed to fetch projects: ${error.message}`); } return (data || []).map((row) => mapProjectRow(row as Record)); } /** * Count total projects */ export async function countProjects(): Promise { const supabase = getServiceSupabase(); const { count, error } = await supabase .from("projects") .select("*", { count: "exact", head: true }); if (error) { console.error("Error counting projects:", error); return 0; } return count || 0; } // Sprint status type const SPRINT_STATUSES = ["planning", "active", "completed", "cancelled"] as const; type SprintStatus = typeof SPRINT_STATUSES[number]; function isSprintStatus(value: unknown): value is SprintStatus { return typeof value === "string" && SPRINT_STATUSES.includes(value as SprintStatus); } function mapSprintRow(row: Record): Sprint { return { id: String(row.id ?? ""), name: toNonEmptyString(row.name) ?? "Untitled Sprint", status: isSprintStatus(row.status) ? row.status : "planning", startDate: toNonEmptyString(row.start_date) ?? new Date().toISOString(), endDate: toNonEmptyString(row.end_date) ?? new Date().toISOString(), projectId: String(row.project_id ?? ""), goal: toNonEmptyString(row.goal), }; } /** * Fetch all sprints from Supabase */ export async function fetchAllSprints(): Promise { const supabase = getServiceSupabase(); const { data, error } = await supabase .from("sprints") .select("*") .order("start_date", { ascending: false }); if (error) { console.error("Error fetching sprints:", error); return []; } return (data || []).map((row) => mapSprintRow(row as Record)); } /** * Fetch sprints for a specific project */ export async function fetchProjectSprints(projectId: string): Promise { const supabase = getServiceSupabase(); const { data, error } = await supabase .from("sprints") .select("*") .eq("project_id", projectId) .order("start_date", { ascending: false }); if (error) { console.error("Error fetching project sprints:", error); return []; } return (data || []).map((row) => mapSprintRow(row as Record)); } /** * Fetch active sprint */ export async function fetchActiveSprint(): Promise { const supabase = getServiceSupabase(); const { data, error } = await supabase .from("sprints") .select("*") .eq("status", "active") .maybeSingle(); if (error) { console.error("Error fetching active sprint:", error); return null; } return data ? mapSprintRow(data as Record) : null; } /** * Count sprints by status */ export async function countSprintsByStatus(): Promise<{ planning: number; active: number; completed: number; total: number }> { const supabase = getServiceSupabase(); const { data, error } = await supabase .from("sprints") .select("status"); if (error) { console.error("Error counting sprints:", error); return { planning: 0, active: 0, completed: 0, total: 0 }; } const counts = { planning: 0, active: 0, completed: 0, total: 0 }; for (const row of data || []) { counts.total++; const status = row.status; if (status === "planning") counts.planning++; else if (status === "active") counts.active++; else if (status === "completed") counts.completed++; } return counts; } /** * Calculate project statistics */ export function calculateProjectStats(project: Project, tasks: Task[]): ProjectStats { const projectTasks = tasks.filter(t => t.projectId === project.id); const completedTasks = projectTasks.filter(t => t.status === "done"); const inProgressTasks = projectTasks.filter(t => t.status === "in-progress"); const urgentTasks = projectTasks.filter(t => t.priority === "urgent" && t.status !== "done"); const highPriorityTasks = projectTasks.filter(t => t.priority === "high" && t.status !== "done"); const totalTasks = projectTasks.length; const progress = totalTasks > 0 ? Math.round((completedTasks.length / totalTasks) * 100) : 0; // Get recent tasks (last 5 updated) const recentTasks = [...projectTasks] .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) .slice(0, 5); return { project, totalTasks, completedTasks: completedTasks.length, inProgressTasks: inProgressTasks.length, urgentTasks: urgentTasks.length, highPriorityTasks: highPriorityTasks.length, progress, recentTasks, }; } /** * Fetch all projects with their statistics */ export async function fetchProjectsWithStats(): Promise { const supabase = getServiceSupabase(); // Fetch projects and tasks in parallel const [projectsResult, tasksResult] = await Promise.all([ supabase.from("projects").select("*").order("created_at", { ascending: true }), supabase.from("tasks").select("*"), ]); if (projectsResult.error) { console.error("Error fetching projects:", projectsResult.error); throw new Error(`Failed to fetch projects: ${projectsResult.error.message}`); } const projects = (projectsResult.data || []).map((row) => mapProjectRow(row as Record)); const tasks = (tasksResult.data || []).map((row) => { 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: (toNonEmptyString(row.status) as Task["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), assigneeId: toNonEmptyString(row.assignee_id), assigneeName: toNonEmptyString(row.assignee_name), dueDate: toNonEmptyString(row.due_date), comments: Array.isArray(row.comments) ? row.comments : [], tags: Array.isArray(row.tags) ? row.tags.filter((tag: unknown): tag is string => typeof tag === "string") : [], attachments: Array.isArray(row.attachments) ? row.attachments : [], } as Task; }); return projects.map(project => calculateProjectStats(project, tasks)); } /** * Get project health status based on various metrics */ export function getProjectHealth(stats: ProjectStats): { status: "healthy" | "warning" | "critical"; label: string; color: string; } { const { progress, urgentTasks, highPriorityTasks, totalTasks } = stats; // Critical: Has urgent tasks or no progress with many open tasks if (urgentTasks > 0 || (totalTasks > 5 && progress === 0)) { return { status: "critical", label: "Needs Attention", color: "text-red-500" }; } // Warning: Has high priority tasks or low progress if (highPriorityTasks > 2 || (totalTasks > 10 && progress < 30)) { return { status: "warning", label: "At Risk", color: "text-yellow-500" }; } // Healthy: Good progress, no urgent issues return { status: "healthy", label: "On Track", color: "text-green-500" }; } /** * Calculate days remaining in sprint */ export function getSprintDaysRemaining(sprint: Sprint): number { const endDate = new Date(sprint.endDate); const today = new Date(); const diffTime = endDate.getTime() - today.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * Format sprint date range */ export function formatSprintDateRange(sprint: Sprint): string { const start = new Date(sprint.startDate); const end = new Date(sprint.endDate); return `${start.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${end.toLocaleDateString("en-US", { month: "short", day: "numeric" })}`; }