469 lines
17 KiB
TypeScript
469 lines
17 KiB
TypeScript
import { DashboardLayout } from "@/components/layout/sidebar";
|
|
import { PageHeader } from "@/components/layout/page-header";
|
|
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 shrink-0">{getStatusIcon(task.status)}</div>
|
|
<div className="flex-1 min-w-0 overflow-hidden">
|
|
<Link
|
|
href={`https://gantt-board.vercel.app/tasks/${encodeURIComponent(task.id)}`}
|
|
target="_blank"
|
|
className="text-sm font-medium block truncate hover:text-primary hover:underline"
|
|
>
|
|
{task.title}
|
|
</Link>
|
|
<div className="flex flex-wrap 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 shrink-0 ${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 className="hover:shadow-md transition-shadow">
|
|
<CardContent className="p-4 sm:p-6">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs sm:text-sm font-medium text-muted-foreground truncate">{title}</p>
|
|
<p className="text-2xl sm:text-3xl font-bold mt-1">{value}</p>
|
|
{subtitle && <p className="text-xs text-muted-foreground mt-1 truncate">{subtitle}</p>}
|
|
</div>
|
|
<div className={`p-2 sm:p-3 rounded-lg shrink-0 ${colorClass}`}>
|
|
<Icon className="w-4 h-4 sm:w-5 sm: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 className="hover:shadow-md transition-shadow">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm sm:text-base">
|
|
<Layers className="w-4 h-4" />
|
|
Tasks by Status
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-3">
|
|
{[
|
|
{ label: "Open", count: counts.open, icon: Circle, color: "bg-muted-foreground", textColor: "text-muted-foreground" },
|
|
{ label: "In Progress", count: counts.inProgress, icon: Clock, color: "bg-blue-500", textColor: "text-blue-500" },
|
|
{ label: "Review", count: counts.review, icon: TrendingUp, color: "bg-purple-500", textColor: "text-purple-500" },
|
|
{ label: "Done", count: counts.done, icon: CheckCircle2, color: "bg-green-500", textColor: "text-green-500" },
|
|
].map((item) => (
|
|
<div key={item.label}>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<div className="flex items-center gap-2">
|
|
<item.icon className={`w-4 h-4 ${item.textColor}`} />
|
|
<span className="text-sm">{item.label}</span>
|
|
</div>
|
|
<span className="text-sm font-medium">{item.count}</span>
|
|
</div>
|
|
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${item.color} rounded-full transition-all`}
|
|
style={{ width: counts.total ? `${(item.count / counts.total) * 100}%` : "0%" }}
|
|
/>
|
|
</div>
|
|
</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-6 sm:space-y-8">
|
|
{/* Header */}
|
|
<PageHeader
|
|
title="Tasks Overview"
|
|
description={
|
|
<>
|
|
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>
|
|
</>
|
|
}
|
|
>
|
|
<Link href="https://gantt-board.vercel.app" target="_blank">
|
|
<Button size="sm" className="w-full sm:w-auto">
|
|
<ExternalLink className="w-4 h-4 mr-2" />
|
|
Open gantt-board
|
|
</Button>
|
|
</Link>
|
|
</PageHeader>
|
|
|
|
{/* 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-3 sm: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 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
{/* Recently Updated */}
|
|
<Card className="hover:shadow-md transition-shadow">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm sm: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 className="hover:shadow-md transition-shadow">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm sm: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 className="hover:shadow-md transition-shadow">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm sm: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-4 sm:gap-6">
|
|
{/* Status Breakdown */}
|
|
<StatusBreakdownCard counts={statusCounts} />
|
|
|
|
{/* Quick Actions */}
|
|
<Card className="lg:col-span-2 hover:shadow-md transition-shadow">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm sm:text-base">
|
|
<ArrowRight className="w-4 h-4" />
|
|
Quick Actions
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
|
<Link href="https://gantt-board.vercel.app" target="_blank" className="block">
|
|
<Button variant="outline" className="w-full justify-start h-auto py-3 sm:py-4">
|
|
<div className="p-2 rounded-lg bg-blue-500/15 mr-3 shrink-0">
|
|
<ExternalLink className="w-4 h-4 sm:w-5 sm:h-5 text-blue-500" />
|
|
</div>
|
|
<div className="text-left min-w-0">
|
|
<p className="font-medium text-sm sm:text-base truncate">Open gantt-board</p>
|
|
<p className="text-xs text-muted-foreground truncate">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-3 sm:py-4">
|
|
<div className="p-2 rounded-lg bg-purple-500/15 mr-3 shrink-0">
|
|
<Layers className="w-4 h-4 sm:w-5 sm:h-5 text-purple-500" />
|
|
</div>
|
|
<div className="text-left min-w-0">
|
|
<p className="font-medium text-sm sm:text-base truncate">View All Tasks</p>
|
|
<p className="text-xs text-muted-foreground truncate">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-3 sm:py-4">
|
|
<div className="p-2 rounded-lg bg-orange-500/15 mr-3 shrink-0">
|
|
<AlertTriangle className="w-4 h-4 sm:w-5 sm:h-5 text-orange-500" />
|
|
</div>
|
|
<div className="text-left min-w-0">
|
|
<p className="font-medium text-sm sm:text-base truncate">View High Priority</p>
|
|
<p className="text-xs text-muted-foreground truncate">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-3 sm:py-4">
|
|
<div className="p-2 rounded-lg bg-green-500/15 mr-3 shrink-0">
|
|
<Clock className="w-4 h-4 sm:w-5 sm:h-5 text-green-500" />
|
|
</div>
|
|
<div className="text-left min-w-0">
|
|
<p className="font-medium text-sm sm:text-base truncate">View In Progress</p>
|
|
<p className="text-xs text-muted-foreground truncate">Currently active tasks</p>
|
|
</div>
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
<Separator className="my-4 sm:my-6" />
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
|
<div className="min-w-0">
|
|
<p className="font-medium text-sm sm:text-base">Task Management</p>
|
|
<p className="text-xs sm: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" className="shrink-0">
|
|
<Button variant="secondary" size="sm">
|
|
Go to gantt-board
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|