235 lines
7.9 KiB
TypeScript
235 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { format, isPast, isToday, isTomorrow, parseISO } from "date-fns";
|
|
import {
|
|
AlertCircle,
|
|
CheckCircle2,
|
|
Clock,
|
|
ExternalLink,
|
|
Calendar,
|
|
User,
|
|
} from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
interface TaskWithDueDate {
|
|
id: string;
|
|
title: string;
|
|
status: string;
|
|
priority: string;
|
|
due_date: string | null;
|
|
assignee_name: string | null;
|
|
project_name: string | null;
|
|
}
|
|
|
|
interface TaskCalendarIntegrationProps {
|
|
maxTasks?: number;
|
|
}
|
|
|
|
function getPriorityColor(priority: string): string {
|
|
switch (priority) {
|
|
case "urgent":
|
|
return "bg-red-500/10 text-red-500 border-red-500/20";
|
|
case "high":
|
|
return "bg-orange-500/10 text-orange-500 border-orange-500/20";
|
|
case "medium":
|
|
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20";
|
|
default:
|
|
return "bg-blue-500/10 text-blue-500 border-blue-500/20";
|
|
}
|
|
}
|
|
|
|
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" />;
|
|
default:
|
|
return <Clock className="w-4 h-4 text-muted-foreground" />;
|
|
}
|
|
}
|
|
|
|
function formatDueDate(dateString: string): { text: string; isOverdue: boolean; isToday: boolean; isTomorrow: boolean } {
|
|
const date = parseISO(dateString);
|
|
const now = new Date();
|
|
|
|
return {
|
|
text: format(date, "MMM d, yyyy"),
|
|
isOverdue: isPast(date) && !isToday(date),
|
|
isToday: isToday(date),
|
|
isTomorrow: isTomorrow(date),
|
|
};
|
|
}
|
|
|
|
export function TaskCalendarIntegration({ maxTasks = 10 }: TaskCalendarIntegrationProps) {
|
|
const [tasks, setTasks] = useState<TaskWithDueDate[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
async function fetchTasksWithDueDates() {
|
|
try {
|
|
// Fetch tasks from Supabase via our API
|
|
const response = await fetch("/api/tasks/with-due-dates");
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch tasks");
|
|
}
|
|
const data = await response.json();
|
|
setTasks(data.slice(0, maxTasks));
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to load tasks");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
fetchTasksWithDueDates();
|
|
}, [maxTasks]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm sm:text-base">
|
|
<Calendar className="w-4 h-4" />
|
|
Tasks with Due Dates
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-center py-8">
|
|
<Clock className="w-6 h-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm sm:text-base">
|
|
<Calendar className="w-4 h-4" />
|
|
Tasks with Due Dates
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-center py-6 text-muted-foreground">
|
|
<AlertCircle className="w-8 h-8 mx-auto mb-2 text-destructive" />
|
|
<p className="text-sm">{error}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const upcomingTasks = tasks.filter((t) => t.status !== "done");
|
|
const overdueTasks = upcomingTasks.filter((t) => t.due_date && formatDueDate(t.due_date).isOverdue);
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2 text-sm sm:text-base">
|
|
<Calendar className="w-4 h-4" />
|
|
Tasks with Due Dates
|
|
</CardTitle>
|
|
{overdueTasks.length > 0 && (
|
|
<Badge variant="destructive" className="text-xs">
|
|
{overdueTasks.length} overdue
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
{tasks.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<Calendar className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
|
<p className="text-sm">No tasks with due dates</p>
|
|
<p className="text-xs mt-1">Tasks with due dates will appear here</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<ScrollArea className="h-[250px] pr-4">
|
|
<div className="space-y-2">
|
|
{tasks.map((task) => {
|
|
const dueInfo = task.due_date ? formatDueDate(task.due_date) : null;
|
|
|
|
return (
|
|
<div
|
|
key={task.id}
|
|
className="p-3 rounded-lg border hover:bg-accent transition-colors"
|
|
>
|
|
<div className="flex items-start gap-2">
|
|
{getStatusIcon(task.status)}
|
|
<div className="flex-1 min-w-0">
|
|
<Link
|
|
href={`https://gantt-board.vercel.app/tasks/${encodeURIComponent(task.id)}`}
|
|
target="_blank"
|
|
className="font-medium text-sm hover:text-primary hover:underline block truncate"
|
|
>
|
|
{task.title}
|
|
</Link>
|
|
<div className="flex flex-wrap items-center gap-2 mt-1">
|
|
{dueInfo && (
|
|
<span
|
|
className={`text-xs flex items-center gap-1 ${
|
|
dueInfo.isOverdue
|
|
? "text-red-500 font-medium"
|
|
: dueInfo.isToday
|
|
? "text-blue-500 font-medium"
|
|
: "text-muted-foreground"
|
|
}`}
|
|
>
|
|
<Clock className="w-3 h-3" />
|
|
{dueInfo.isToday
|
|
? "Today"
|
|
: dueInfo.isTomorrow
|
|
? "Tomorrow"
|
|
: dueInfo.text}
|
|
</span>
|
|
)}
|
|
{task.assignee_name && (
|
|
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
|
<User className="w-3 h-3" />
|
|
{task.assignee_name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-[10px] shrink-0 ${getPriorityColor(task.priority)}`}
|
|
>
|
|
{task.priority}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
<div className="mt-4 pt-4 border-t">
|
|
<Link
|
|
href="https://gantt-board.vercel.app/tasks"
|
|
target="_blank"
|
|
>
|
|
<Button variant="ghost" size="sm" className="w-full gap-2">
|
|
View all tasks
|
|
<ExternalLink className="w-3 h-3" />
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|