mission-control/app/tasks/page.tsx

494 lines
17 KiB
TypeScript

import { DashboardLayout } from "@/components/layout/sidebar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
CheckCircle2,
Circle,
Clock,
AlertCircle,
TrendingUp,
ExternalLink,
ArrowRight,
Calendar,
AlertTriangle,
User,
Layers,
} from "lucide-react";
import Link from "next/link";
import {
fetchRecentlyUpdatedTasks,
fetchRecentlyCompletedTasks,
fetchHighPriorityOpenTasks,
getTaskStatusCounts,
countHighPriorityTasks,
countOverdueTasks,
Task,
} from "@/lib/data/tasks";
// Force dynamic rendering to fetch fresh data from Supabase on each request
export const dynamic = "force-dynamic";
// Helper functions
function formatRelativeTime(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
function getPriorityColor(priority: string): string {
switch (priority) {
case "urgent":
return "bg-red-500/15 text-red-400 border-red-500/30";
case "high":
return "bg-orange-500/15 text-orange-400 border-orange-500/30";
case "medium":
return "bg-yellow-500/15 text-yellow-400 border-yellow-500/30";
default:
return "bg-blue-500/15 text-blue-400 border-blue-500/30";
}
}
function getStatusIcon(status: string) {
switch (status) {
case "done":
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
case "in-progress":
return <Clock className="w-4 h-4 text-blue-500" />;
case "review":
case "validate":
return <Circle className="w-4 h-4 text-purple-500" />;
default:
return <Circle className="w-4 h-4 text-muted-foreground" />;
}
}
function getStatusLabel(status: string): string {
switch (status) {
case "open":
case "todo":
return "Open";
case "in-progress":
return "In Progress";
case "review":
case "validate":
return "Review";
case "done":
return "Done";
case "blocked":
return "Blocked";
case "archived":
return "Archived";
case "canceled":
return "Canceled";
default:
return status;
}
}
// Task list item component
function TaskListItem({ task }: { task: Task }) {
return (
<div className="flex items-start gap-3 py-3 border-b border-border last:border-0">
<div className="mt-0.5">{getStatusIcon(task.status)}</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{task.title}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-muted-foreground">
{formatRelativeTime(task.updatedAt)}
</span>
{task.assigneeName && (
<span className="text-xs text-muted-foreground flex items-center gap-1">
<User className="w-3 h-3" />
{task.assigneeName}
</span>
)}
</div>
</div>
<Badge variant="outline" className={`text-xs ${getPriorityColor(task.priority)}`}>
{task.priority}
</Badge>
</div>
);
}
// Stat card component
function StatCard({
title,
value,
subtitle,
icon: Icon,
colorClass,
}: {
title: string;
value: string | number;
subtitle?: string;
icon: React.ElementType;
colorClass: string;
}) {
return (
<Card>
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">{title}</p>
<p className="text-3xl font-bold mt-2">{value}</p>
{subtitle && <p className="text-xs text-muted-foreground mt-1">{subtitle}</p>}
</div>
<div className={`p-3 rounded-lg ${colorClass}`}>
<Icon className="w-5 h-5" />
</div>
</div>
</CardContent>
</Card>
);
}
// Status breakdown card
function StatusBreakdownCard({ counts }: { counts: { open: number; inProgress: number; review: number; done: number; total: number } }) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Layers className="w-4 h-4" />
Tasks by Status
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Circle className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">Open</span>
</div>
<span className="text-sm font-medium">{counts.open}</span>
</div>
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
<div
className="h-full bg-muted-foreground rounded-full"
style={{ width: counts.total ? `${(counts.open / counts.total) * 100}%` : "0%" }}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-blue-500" />
<span className="text-sm">In Progress</span>
</div>
<span className="text-sm font-medium">{counts.inProgress}</span>
</div>
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full"
style={{ width: counts.total ? `${(counts.inProgress / counts.total) * 100}%` : "0%" }}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-purple-500" />
<span className="text-sm">Review</span>
</div>
<span className="text-sm font-medium">{counts.review}</span>
</div>
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
<div
className="h-full bg-purple-500 rounded-full"
style={{ width: counts.total ? `${(counts.review / counts.total) * 100}%` : "0%" }}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-green-500" />
<span className="text-sm">Done</span>
</div>
<span className="text-sm font-medium">{counts.done}</span>
</div>
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
<div
className="h-full bg-green-500 rounded-full"
style={{ width: counts.total ? `${(counts.done / counts.total) * 100}%` : "0%" }}
/>
</div>
</div>
<Separator />
<div className="flex items-center justify-between pt-2">
<span className="text-sm font-medium">Total</span>
<span className="text-lg font-bold">{counts.total}</span>
</div>
</CardContent>
</Card>
);
}
export default async function TasksOverviewPage() {
// Fetch all data in parallel with error handling
let statusCounts = { open: 0, inProgress: 0, review: 0, done: 0, total: 0 };
let highPriorityCount = 0;
let overdueCount = 0;
let recentlyUpdated: Task[] = [];
let recentlyCompleted: Task[] = [];
let highPriorityOpen: Task[] = [];
let errorMessage: string | null = null;
try {
[
statusCounts,
highPriorityCount,
overdueCount,
recentlyUpdated,
recentlyCompleted,
highPriorityOpen,
] = await Promise.all([
getTaskStatusCounts(),
countHighPriorityTasks(),
countOverdueTasks(),
fetchRecentlyUpdatedTasks(5),
fetchRecentlyCompletedTasks(5),
fetchHighPriorityOpenTasks(5),
]);
} catch (error) {
console.error("Error fetching tasks:", error);
errorMessage = error instanceof Error ? error.message : "Failed to load tasks";
}
return (
<DashboardLayout>
<div className="space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Tasks Overview</h1>
<p className="text-muted-foreground mt-1">
Mission Control view of all tasks. Manage work in{" "}
<Link
href="https://gantt-board.vercel.app"
target="_blank"
className="text-primary hover:underline inline-flex items-center gap-1"
>
gantt-board
<ExternalLink className="w-3 h-3" />
</Link>
</p>
</div>
<Link href="https://gantt-board.vercel.app" target="_blank">
<Button>
<ExternalLink className="w-4 h-4 mr-2" />
Open in gantt-board
</Button>
</Link>
</div>
{/* Error Message */}
{errorMessage && (
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 text-red-400">
<p className="font-medium">Error loading tasks</p>
<p className="text-sm mt-1">{errorMessage}</p>
<p className="text-sm mt-2">Make sure you are logged into gantt-board and have access to the Supabase database.</p>
</div>
)}
{/* Stats Section */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Total Tasks"
value={statusCounts.total}
subtitle="Across all projects"
icon={Layers}
colorClass="bg-blue-500/15 text-blue-500"
/>
<StatCard
title="In Progress"
value={statusCounts.inProgress}
subtitle={`${statusCounts.open} open, ${statusCounts.review} in review`}
icon={Clock}
colorClass="bg-purple-500/15 text-purple-500"
/>
<StatCard
title="High Priority"
value={highPriorityCount}
subtitle="Needs attention"
icon={AlertTriangle}
colorClass="bg-orange-500/15 text-orange-500"
/>
<StatCard
title="Overdue"
value={overdueCount}
subtitle="Past due date"
icon={Calendar}
colorClass="bg-red-500/15 text-red-500"
/>
</div>
{/* Recent Activity Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Recently Updated */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Clock className="w-4 h-4" />
Recently Updated
</CardTitle>
</CardHeader>
<CardContent>
{recentlyUpdated.length > 0 ? (
<div className="divide-y divide-border">
{recentlyUpdated.map((task) => (
<TaskListItem key={task.id} task={task} />
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-8">
No recently updated tasks
</p>
)}
</CardContent>
</Card>
{/* Recently Completed */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<CheckCircle2 className="w-4 h-4" />
Recently Completed
</CardTitle>
</CardHeader>
<CardContent>
{recentlyCompleted.length > 0 ? (
<div className="divide-y divide-border">
{recentlyCompleted.map((task) => (
<TaskListItem key={task.id} task={task} />
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-8">
No recently completed tasks
</p>
)}
</CardContent>
</Card>
{/* High Priority Open Tasks */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<AlertCircle className="w-4 h-4" />
High Priority Open
</CardTitle>
</CardHeader>
<CardContent>
{highPriorityOpen.length > 0 ? (
<div className="divide-y divide-border">
{highPriorityOpen.map((task) => (
<TaskListItem key={task.id} task={task} />
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-8">
No high priority open tasks
</p>
)}
</CardContent>
</Card>
</div>
{/* Bottom Section: Status Breakdown + Quick Actions */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Status Breakdown */}
<StatusBreakdownCard counts={statusCounts} />
{/* Quick Actions */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<ArrowRight className="w-4 h-4" />
Quick Actions
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Link href="https://gantt-board.vercel.app" target="_blank" className="block">
<Button variant="outline" className="w-full justify-start h-auto py-4">
<div className="p-2 rounded-lg bg-blue-500/15 mr-3">
<ExternalLink className="w-5 h-5 text-blue-500" />
</div>
<div className="text-left">
<p className="font-medium">Open gantt-board</p>
<p className="text-xs text-muted-foreground">Manage tasks and projects</p>
</div>
</Button>
</Link>
<Link href="https://gantt-board.vercel.app/tasks" target="_blank" className="block">
<Button variant="outline" className="w-full justify-start h-auto py-4">
<div className="p-2 rounded-lg bg-purple-500/15 mr-3">
<Layers className="w-5 h-5 text-purple-500" />
</div>
<div className="text-left">
<p className="font-medium">View All Tasks</p>
<p className="text-xs text-muted-foreground">Full task list in gantt-board</p>
</div>
</Button>
</Link>
<Link href="https://gantt-board.vercel.app/tasks?priority=high,urgent" target="_blank" className="block">
<Button variant="outline" className="w-full justify-start h-auto py-4">
<div className="p-2 rounded-lg bg-orange-500/15 mr-3">
<AlertTriangle className="w-5 h-5 text-orange-500" />
</div>
<div className="text-left">
<p className="font-medium">View High Priority</p>
<p className="text-xs text-muted-foreground">Urgent and high priority tasks</p>
</div>
</Button>
</Link>
<Link href="https://gantt-board.vercel.app/tasks?status=in-progress" target="_blank" className="block">
<Button variant="outline" className="w-full justify-start h-auto py-4">
<div className="p-2 rounded-lg bg-green-500/15 mr-3">
<Clock className="w-5 h-5 text-green-500" />
</div>
<div className="text-left">
<p className="font-medium">View In Progress</p>
<p className="text-xs text-muted-foreground">Currently active tasks</p>
</div>
</Button>
</Link>
</div>
<Separator className="my-6" />
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div>
<p className="font-medium">Task Management</p>
<p className="text-sm text-muted-foreground">
Mission Control is read-only. All task creation and editing happens in gantt-board.
</p>
</div>
<Link href="https://gantt-board.vercel.app" target="_blank">
<Button variant="secondary">
Go to gantt-board
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
</div>
</DashboardLayout>
);
}