import { getServiceSupabase } from "@/lib/supabase/client"; import { Task, fetchAllTasks } from "./tasks"; import { Project, fetchAllProjects } from "./projects"; // ============================================================================ // Types // ============================================================================ export interface ProgressMetric { id: string; label: string; current: number; target: number; unit: string; percentage: number; icon: string; color: string; } export interface Milestone { id: string; title: string; description?: string; completedAt: string; category: "ios" | "financial" | "travel" | "family" | "business"; icon: string; } export interface NextStep { id: string; title: string; priority: "high" | "urgent"; projectName?: string; dueDate?: string; ganttBoardUrl: string; } export interface MissionProgress { retirement: ProgressMetric; iosPortfolio: ProgressMetric; sideHustleRevenue: ProgressMetric; travelFund: ProgressMetric; milestones: Milestone[]; nextSteps: NextStep[]; } // ============================================================================ // Constants // ============================================================================ // Target goals const TARGET_APPS = 20; const TARGET_REVENUE = 10000; // $10K MRR target const TARGET_TRAVEL_FUND = 50000; // $50K travel fund const TARGET_RETIREMENT_TASKS = 100; // Tasks completed as proxy for progress // iOS-related keywords for identifying app projects const IOS_KEYWORDS = ["ios", "app", "swift", "mobile", "iphone", "ipad", "xcode"]; // ============================================================================ // Helper Functions // ============================================================================ /** * Check if a project is iOS-related */ function isIOSProject(project: Project): boolean { const nameLower = project.name.toLowerCase(); const descLower = (project.description || "").toLowerCase(); return IOS_KEYWORDS.some(keyword => nameLower.includes(keyword) || descLower.includes(keyword) ); } /** * Check if a task is iOS-related */ function isIOSTask(task: Task): boolean { const titleLower = task.title.toLowerCase(); const descLower = (task.description || "").toLowerCase(); const hasIOSTag = task.tags.some(tag => ["ios", "app", "swift", "mobile"].includes(tag.toLowerCase()) ); return hasIOSTag || IOS_KEYWORDS.some(keyword => titleLower.includes(keyword) || descLower.includes(keyword) ); } /** * Calculate percentage with bounds */ function calculatePercentage(current: number, target: number): number { if (target === 0) return 0; return Math.min(100, Math.max(0, Math.round((current / target) * 100))); } /** * Format currency */ function formatCurrency(amount: number): string { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }).format(amount); } // ============================================================================ // Progress Metric Functions // ============================================================================ /** * Get retirement progress based on completed tasks as a proxy * In future, this could be based on actual financial data */ export async function getRetirementProgress(): Promise { const supabase = getServiceSupabase(); // Count completed tasks as a proxy for "progress toward freedom" const { count: completedTasks, error } = await supabase .from("tasks") .select("*", { count: "exact", head: true }) .eq("status", "done"); if (error) { console.error("Error calculating retirement progress:", error); } const current = completedTasks || 0; const percentage = calculatePercentage(current, TARGET_RETIREMENT_TASKS); return { id: "retirement", label: "Freedom Progress", current, target: TARGET_RETIREMENT_TASKS, unit: "tasks", percentage, icon: "Target", color: "from-emerald-500 to-teal-500", }; } /** * Get iOS portfolio progress * Counts completed iOS projects and tasks */ export async function getiOSPortfolioProgress(): Promise { const [projects, tasks] = await Promise.all([ fetchAllProjects(), fetchAllTasks(), ]); // Count iOS projects const iosProjects = projects.filter(isIOSProject); // Count completed iOS tasks as additional progress const completedIOSTasks = tasks.filter(t => isIOSTask(t) && t.status === "done" ).length; // Weight: 1 project = 1 app, 10 completed tasks = 1 app progress const taskContribution = Math.floor(completedIOSTasks / 10); const current = Math.min(iosProjects.length + taskContribution, TARGET_APPS); const percentage = calculatePercentage(current, TARGET_APPS); return { id: "ios-portfolio", label: "iOS Portfolio", current, target: TARGET_APPS, unit: "apps", percentage, icon: "Smartphone", color: "from-blue-500 to-cyan-500", }; } /** * Get side hustle revenue progress * Placeholder - will be replaced with real revenue tracking */ export async function getSideHustleRevenue(): Promise { // Placeholder: For now, calculate based on completed milestones // In future, this will pull from actual revenue data const supabase = getServiceSupabase(); const { data: doneTasks, error } = await supabase .from("tasks") .select("*") .eq("status", "done"); if (error) { console.error("Error fetching revenue milestones:", error); } // Filter for tasks with revenue or milestone tags const milestones = (doneTasks || []).filter(task => { const tags = (task.tags || []) as string[]; return tags.some(tag => tag.toLowerCase() === "revenue" || tag.toLowerCase() === "milestone" ); }); // Estimate: each revenue milestone = $500 progress (placeholder logic) const milestoneCount = milestones.length; const estimatedRevenue = milestoneCount * 500; return { id: "revenue", label: "Side Hustle Revenue", current: estimatedRevenue, target: TARGET_REVENUE, unit: "MRR", percentage: calculatePercentage(estimatedRevenue, TARGET_REVENUE), icon: "DollarSign", color: "from-amber-500 to-orange-500", }; } /** * Get travel fund progress * Placeholder - will be replaced with actual savings tracking */ export async function getTravelFundProgress(): Promise { // Placeholder: Calculate based on "travel" tagged completed tasks const supabase = getServiceSupabase(); const { data: doneTasks, error } = await supabase .from("tasks") .select("*") .eq("status", "done"); if (error) { console.error("Error fetching travel progress:", error); } // Filter for tasks with travel tag const travelTasks = (doneTasks || []).filter(task => { const tags = (task.tags || []) as string[]; return tags.some(tag => tag.toLowerCase() === "travel"); }); // Estimate: each travel task = $500 saved (placeholder) const estimatedSavings = travelTasks.length * 500; return { id: "travel", label: "Travel Fund", current: estimatedSavings, target: TARGET_TRAVEL_FUND, unit: "saved", percentage: calculatePercentage(estimatedSavings, TARGET_TRAVEL_FUND), icon: "Plane", color: "from-purple-500 to-pink-500", }; } // ============================================================================ // Milestones Functions // ============================================================================ /** * Get mission milestones from completed tasks tagged with "milestone" or "mission" */ export async function getMissionMilestones(): Promise { const supabase = getServiceSupabase(); // Fetch all completed tasks and filter for tags in code // (Supabase JSON filtering can be tricky with different setups) const { data, error } = await supabase .from("tasks") .select("*") .eq("status", "done"); if (error) { console.error("Error fetching mission milestones:", error); return []; } // Filter for tasks with milestone or mission tags const milestoneTasks = (data || []).filter(task => { const tags = (task.tags || []) as string[]; return tags.some(tag => tag.toLowerCase() === "milestone" || tag.toLowerCase() === "mission" ); }); // Sort by completion date (updated_at) descending const sortedData = milestoneTasks.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() ); return sortedData.map((task, index) => { // Determine category based on tags and title let category: Milestone["category"] = "business"; const titleLower = task.title.toLowerCase(); const tagsLower = (task.tags || []).map((t: string) => t.toLowerCase()); if (tagsLower.includes("ios") || tagsLower.includes("app") || titleLower.includes("app")) { category = "ios"; } else if (tagsLower.includes("travel") || titleLower.includes("travel")) { category = "travel"; } else if (tagsLower.includes("family") || titleLower.includes("family")) { category = "family"; } else if (tagsLower.includes("revenue") || tagsLower.includes("money") || titleLower.includes("$")) { category = "financial"; } // Determine icon based on category const iconMap: Record = { ios: "Smartphone", financial: "DollarSign", travel: "Plane", family: "Heart", business: "Briefcase", }; return { id: task.id, title: task.title, description: task.description, completedAt: task.updated_at, category, icon: iconMap[category], }; }); } // ============================================================================ // Next Steps Functions // ============================================================================ /** * Get next mission steps - high priority tasks that advance the mission */ export async function getNextMissionSteps(): Promise { const supabase = getServiceSupabase(); // Fetch high/urgent priority tasks that are not done const { data: tasks, error: tasksError } = await supabase .from("tasks") .select("*") .in("priority", ["high", "urgent"]) .neq("status", "done") .order("due_date", { ascending: true }) .limit(6); if (tasksError) { console.error("Error fetching next mission steps:", tasksError); return []; } // Fetch projects for names const { data: projects, error: projectsError } = await supabase .from("projects") .select("id, name"); if (projectsError) { console.error("Error fetching projects:", projectsError); } const projectMap = new Map((projects || []).map(p => [p.id, p.name])); return (tasks || []).map(task => ({ id: task.id, title: task.title, priority: task.priority as "high" | "urgent", projectName: projectMap.get(task.project_id), dueDate: task.due_date, ganttBoardUrl: `https://gantt-board.vercel.app/?task=${task.id}`, })); } // ============================================================================ // Combined Fetch // ============================================================================ /** * Fetch all mission progress data in parallel */ export async function fetchMissionProgress(): Promise { const [ retirement, iosPortfolio, sideHustleRevenue, travelFund, milestones, nextSteps, ] = await Promise.all([ getRetirementProgress(), getiOSPortfolioProgress(), getSideHustleRevenue(), getTravelFundProgress(), getMissionMilestones(), getNextMissionSteps(), ]); return { retirement, iosPortfolio, sideHustleRevenue, travelFund, milestones, nextSteps, }; }