mission-control/lib/data/mission.ts

236 lines
7.3 KiB
TypeScript

import { getGanttTaskUrl } from "@/lib/config/sites";
import { Task, fetchAllTasks } from "./tasks";
import { Project, fetchAllProjects } from "./projects";
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[];
}
const TARGET_APPS = 20;
const TARGET_REVENUE = 10000;
const TARGET_TRAVEL_FUND = 50000;
const TARGET_RETIREMENT_TASKS = 100;
const IOS_KEYWORDS = ["ios", "app", "swift", "mobile", "iphone", "ipad", "xcode"];
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));
}
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));
}
function calculatePercentage(current: number, target: number): number {
if (target === 0) return 0;
return Math.min(100, Math.max(0, Math.round((current / target) * 100)));
}
function byUpdatedDesc(a: Task, b: Task): number {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
}
export async function getRetirementProgress(): Promise<ProgressMetric> {
const tasks = await fetchAllTasks();
const current = tasks.filter((task) => task.status === "done").length;
return {
id: "retirement",
label: "Freedom Progress",
current,
target: TARGET_RETIREMENT_TASKS,
unit: "tasks",
percentage: calculatePercentage(current, TARGET_RETIREMENT_TASKS),
icon: "Target",
color: "from-emerald-500 to-teal-500",
};
}
export async function getiOSPortfolioProgress(): Promise<ProgressMetric> {
const [projects, tasks] = await Promise.all([fetchAllProjects(), fetchAllTasks()]);
const iosProjects = projects.filter(isIOSProject);
const completedIOSTasks = tasks.filter((task) => isIOSTask(task) && task.status === "done").length;
const taskContribution = Math.floor(completedIOSTasks / 10);
const current = Math.min(iosProjects.length + taskContribution, TARGET_APPS);
return {
id: "ios-portfolio",
label: "iOS Portfolio",
current,
target: TARGET_APPS,
unit: "apps",
percentage: calculatePercentage(current, TARGET_APPS),
icon: "Smartphone",
color: "from-blue-500 to-cyan-500",
};
}
export async function getSideHustleRevenue(): Promise<ProgressMetric> {
const tasks = await fetchAllTasks();
const doneTasks = tasks.filter((task) => task.status === "done");
const milestones = doneTasks.filter((task) =>
task.tags.some((tag) => {
const normalized = tag.toLowerCase();
return normalized === "revenue" || normalized === "milestone";
})
);
const estimatedRevenue = milestones.length * 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",
};
}
export async function getTravelFundProgress(): Promise<ProgressMetric> {
const tasks = await fetchAllTasks();
const doneTasks = tasks.filter((task) => task.status === "done");
const travelTasks = doneTasks.filter((task) => task.tags.some((tag) => tag.toLowerCase() === "travel"));
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",
};
}
export async function getMissionMilestones(): Promise<Milestone[]> {
const tasks = await fetchAllTasks();
const milestoneTasks = tasks
.filter((task) => task.status === "done")
.filter((task) =>
task.tags.some((tag) => {
const normalized = tag.toLowerCase();
return normalized === "milestone" || normalized === "mission";
})
)
.sort(byUpdatedDesc);
return milestoneTasks.map((task) => {
let category: Milestone["category"] = "business";
const titleLower = task.title.toLowerCase();
const tagsLower = task.tags.map((tag) => tag.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";
}
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.updatedAt,
category,
icon: iconMap[category],
};
});
}
export async function getNextMissionSteps(): Promise<NextStep[]> {
const [tasks, projects] = await Promise.all([fetchAllTasks(), fetchAllProjects()]);
const projectMap = new Map(projects.map((project) => [project.id, project.name]));
const prioritized = tasks
.filter((task) => (task.priority === "high" || task.priority === "urgent") && task.status !== "done")
.sort((a, b) => {
const aDue = a.dueDate ? new Date(a.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
const bDue = b.dueDate ? new Date(b.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
if (aDue !== bDue) return aDue - bDue;
return byUpdatedDesc(a, b);
})
.slice(0, 6);
return prioritized.map((task) => ({
id: task.id,
title: task.title,
priority: task.priority as "high" | "urgent",
projectName: projectMap.get(task.projectId),
dueDate: task.dueDate,
ganttBoardUrl: getGanttTaskUrl(task.id),
}));
}
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,
};
}