NeahFront7/components/flow.tsx
2025-04-12 13:10:19 +02:00

211 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}