mission-control/lib/data/mission.ts
OpenClaw Bot 86060a0585 Phase 3: Enhanced Mission page with real progress tracking
- Added lib/data/mission.ts with progress calculations
- Transformed Mission page with progress dashboard
- 4 progress bars: Freedom, iOS Portfolio, Side Hustle, Travel Fund
- Milestones timeline with category badges
- Next Steps section with high priority tasks
- Shimmer animations on progress bars
- Revalidates every 5 minutes
2026-02-21 23:28:41 -06:00

410 lines
12 KiB
TypeScript

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<ProgressMetric> {
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<ProgressMetric> {
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<ProgressMetric> {
// 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<ProgressMetric> {
// 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<Milestone[]> {
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<Milestone["category"], string> = {
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<NextStep[]> {
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<MissionProgress> {
const [
retirement,
iosPortfolio,
sideHustleRevenue,
travelFund,
milestones,
nextSteps,
] = await Promise.all([
getRetirementProgress(),
getiOSPortfolioProgress(),
getSideHustleRevenue(),
getTravelFundProgress(),
getMissionMilestones(),
getNextMissionSteps(),
]);
return {
retirement,
iosPortfolio,
sideHustleRevenue,
travelFund,
milestones,
nextSteps,
};
}