211 lines
7.0 KiB
TypeScript
211 lines
7.0 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, useCallback } from "react";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { RefreshCw, ChevronDown, Filter } from "lucide-react";
|
||
import { useRouter } from "next/navigation";
|
||
import { useSession } from "next-auth/react";
|
||
|
||
interface Task {
|
||
id: string;
|
||
headline: string;
|
||
projectName: string;
|
||
dueDate: string;
|
||
status: string;
|
||
details?: string;
|
||
}
|
||
|
||
interface TaskGroup {
|
||
name: string;
|
||
count: number;
|
||
tasks: Task[];
|
||
}
|
||
|
||
export function Flow() {
|
||
const [taskGroups, setTaskGroups] = useState<TaskGroup[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
const [retryTimeout, setRetryTimeout] = useState<NodeJS.Timeout | null>(null);
|
||
const router = useRouter();
|
||
const { data: session } = useSession();
|
||
|
||
const fetchTasks = useCallback(async (isRefresh = false, retryCount = 0) => {
|
||
try {
|
||
if (isRefresh) {
|
||
setRefreshing(true);
|
||
}
|
||
|
||
if (retryTimeout) {
|
||
clearTimeout(retryTimeout);
|
||
setRetryTimeout(null);
|
||
}
|
||
|
||
const response = await fetch('/api/leantime/tasks', {
|
||
cache: 'no-store',
|
||
next: { revalidate: 0 },
|
||
});
|
||
|
||
if (response.status === 401) {
|
||
setError('Session expired. Please sign in again.');
|
||
return;
|
||
}
|
||
|
||
if (response.status === 429) {
|
||
const retryAfter = response.headers.get('Retry-After');
|
||
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.min(1000 * Math.pow(2, retryCount), 60000);
|
||
setError(`Rate limit exceeded. Retrying in ${Math.round(waitTime / 1000)} seconds...`);
|
||
const timeout = setTimeout(() => {
|
||
fetchTasks(isRefresh, retryCount + 1);
|
||
}, waitTime);
|
||
setRetryTimeout(timeout);
|
||
return;
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch tasks');
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// Group tasks by status
|
||
const groups: { [key: string]: Task[] } = {};
|
||
data.tasks.forEach((task: Task) => {
|
||
const status = task.status || 'No Status';
|
||
if (!groups[status]) {
|
||
groups[status] = [];
|
||
}
|
||
groups[status].push(task);
|
||
});
|
||
|
||
// Convert to array format with counts
|
||
const formattedGroups = Object.entries(groups).map(([name, tasks]) => ({
|
||
name,
|
||
count: tasks.length,
|
||
tasks
|
||
}));
|
||
|
||
setTaskGroups(formattedGroups);
|
||
setError(null);
|
||
} catch (err) {
|
||
console.error('Error fetching tasks:', err);
|
||
setError(err instanceof Error ? err.message : 'Failed to fetch tasks');
|
||
} finally {
|
||
setLoading(false);
|
||
setRefreshing(false);
|
||
}
|
||
}, [retryTimeout]);
|
||
|
||
useEffect(() => {
|
||
if (session) {
|
||
fetchTasks();
|
||
const interval = setInterval(() => fetchTasks(), 300000);
|
||
return () => {
|
||
clearInterval(interval);
|
||
if (retryTimeout) {
|
||
clearTimeout(retryTimeout);
|
||
}
|
||
};
|
||
}
|
||
}, [session, fetchTasks, retryTimeout]);
|
||
|
||
return (
|
||
<Card
|
||
className="transition-transform duration-500 ease-in-out transform hover:scale-105 cursor-pointer bg-white/50 backdrop-blur-sm h-full"
|
||
onClick={() => router.push('/flow')}
|
||
>
|
||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||
<CardTitle className="text-lg font-semibold flex items-center gap-2">
|
||
📋 My ToDos
|
||
</CardTitle>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
fetchTasks(true);
|
||
}}
|
||
disabled={refreshing || !!retryTimeout}
|
||
className={refreshing ? 'animate-spin' : ''}
|
||
>
|
||
<RefreshCw className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-4">
|
||
{loading && <p className="text-center text-muted-foreground">Loading tasks...</p>}
|
||
{error && (
|
||
<div className="text-center">
|
||
<p className="text-red-500">Error: {error}</p>
|
||
{!retryTimeout && (
|
||
<Button
|
||
variant="outline"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
fetchTasks(true);
|
||
}}
|
||
className="mt-2"
|
||
>
|
||
Try Again
|
||
</Button>
|
||
)}
|
||
</div>
|
||
)}
|
||
{!loading && !error && (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||
<Button variant="ghost" size="sm" className="text-gray-500">
|
||
<Filter className="h-4 w-4 mr-2" />
|
||
Group By: Due Date
|
||
</Button>
|
||
<Button variant="ghost" size="sm" className="text-gray-500">
|
||
Filters
|
||
</Button>
|
||
</div>
|
||
{taskGroups.length === 0 ? (
|
||
<p className="text-center text-muted-foreground">No tasks found</p>
|
||
) : (
|
||
taskGroups.map((group) => (
|
||
<div key={group.name} className="space-y-2">
|
||
<div className="flex items-center gap-2 text-gray-700">
|
||
<ChevronDown className="h-4 w-4" />
|
||
{group.name === 'overdue' && <span className="text-red-500">🔥</span>}
|
||
<h3 className="font-medium">
|
||
{group.name.charAt(0).toUpperCase() + group.name.slice(1)} ({group.count})
|
||
</h3>
|
||
</div>
|
||
<div className="space-y-2 pl-6">
|
||
{group.tasks.map((task) => (
|
||
<div
|
||
key={task.id}
|
||
className="relative pl-4 py-2 hover:bg-gray-50 rounded-lg transition-colors"
|
||
>
|
||
{group.name === 'overdue' && (
|
||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-red-500 rounded-full" />
|
||
)}
|
||
<div className="space-y-1">
|
||
<div className="font-medium text-gray-700">{task.projectName}</div>
|
||
<div className="text-sm">
|
||
{task.headline}
|
||
{task.details && (
|
||
<span className="text-gray-500"> // {task.details}</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center text-sm text-gray-500">
|
||
<span>🗓️ {new Date(task.dueDate).toLocaleDateString()}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|