diff --git a/app/api/leantime/status-labels/route.ts b/app/api/leantime/status-labels/route.ts index 43f78e77..4f2f9b23 100644 --- a/app/api/leantime/status-labels/route.ts +++ b/app/api/leantime/status-labels/route.ts @@ -2,6 +2,19 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { NextResponse } from "next/server"; +interface StatusLabel { + name: string; + class: string; + statusType: string; + kanbanCol: boolean | string; + sortKey: number; +} + +interface Project { + projectId: string; + labels: StatusLabel[]; +} + // Simple in-memory cache for user IDs and status labels const userCache = new Map(); const statusLabelsCache = new Map(); @@ -81,90 +94,55 @@ export async function GET() { 'X-API-Key': process.env.LEANTIME_TOKEN || '', }, body: JSON.stringify({ - method: 'leantime.rpc.Tickets.Tickets.getAllStatusLabelsByUserId', jsonrpc: '2.0', + method: 'leantime.rpc.Tickets.Tickets.getAllStatusLabelsByUserId', id: 1, params: { - userId: leantimeUserId + userId: session.user.id } }) }); - const responseText = await response.text(); - console.log('Leantime API Response Status:', response.status); - console.log('Leantime API Response Headers:', response.headers); - console.log('Leantime API Response Body:', responseText); - if (!response.ok) { - if (response.status === 429) { - const retryAfter = response.headers.get('retry-after') || '60'; - return NextResponse.json( - { error: "Rate limit exceeded. Please try again later." }, - { - status: 429, - headers: { - 'Retry-After': retryAfter - } - } - ); - } - if (response.status === 401) { - return NextResponse.json({ error: "Unauthorized access to Leantime API" }, { status: 401 }); - } - if (response.status === 403) { - return NextResponse.json({ error: "Forbidden access to Leantime API" }, { status: 403 }); - } - throw new Error(`Leantime API returned ${response.status}: ${responseText}`); + throw new Error('Failed to fetch status labels from Leantime'); } - let data; - try { - data = JSON.parse(responseText); - } catch (e) { - console.error('Failed to parse Leantime response:', e); - throw new Error('Invalid JSON response from Leantime'); - } + const data = await response.json(); if (!data.result) { - console.log('No result in Leantime response'); return NextResponse.json({ projects: [] }); } - // Transform the response into a more structured format while maintaining project grouping - const transformedProjects = Object.entries(data.result as Record>).map(([projectId, labels]) => { - // Convert the labels object to an array and sort by sortKey - const sortedLabels = Object.entries(labels).map(([id, label]) => ({ - id, + // Transform the response into our desired format + const projects: Project[] = Object.entries(data.result).map(([projectId, statusLabels]: [string, any]) => { + const labels = Object.entries(statusLabels).map(([_, label]: [string, any]) => ({ name: label.name, - statusType: label.statusType, class: label.class, - sortKey: Number(label.sortKey) || 0, - kanbanCol: label.kanbanCol - })).sort((a, b) => a.sortKey - b.sortKey); + statusType: label.statusType, + kanbanCol: label.kanbanCol, + sortKey: parseInt(label.sortKey) + })); + + // Sort labels by sortKey + labels.sort((a, b) => a.sortKey - b.sortKey); return { projectId, - labels: sortedLabels + labels }; }); - // Sort projects by ID for consistency - transformedProjects.sort((a, b) => a.projectId.localeCompare(b.projectId)); - // Cache the transformed data statusLabelsCache.set(leantimeUserId, { - data: transformedProjects, + data: projects, timestamp: Date.now() }); - return NextResponse.json({ projects: transformedProjects }); + return NextResponse.json({ projects }); } catch (error) { - console.error('Detailed error in status labels fetch:', error); + console.error('Error fetching status labels:', error); return NextResponse.json( - { - error: "Failed to fetch status labels", - details: error instanceof Error ? error.message : 'Unknown error' - }, + { error: "Failed to fetch status labels" }, { status: 500 } ); } diff --git a/components/flow.tsx b/components/flow.tsx index 6f46f221..d951b038 100644 --- a/components/flow.tsx +++ b/components/flow.tsx @@ -1,208 +1,130 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState } 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"; +import { RefreshCw } from "lucide-react"; -interface Task { - id: string; - headline: string; - projectName: string; - dueDate: string; - status: string; - details?: string; +interface StatusLabel { + name: string; + class: string; + statusType: string; + kanbanCol: boolean | string; + sortKey: number; } -interface TaskGroup { - name: string; - count: number; - tasks: Task[]; +interface Project { + projectId: string; + labels: StatusLabel[]; } export function Flow() { - const [taskGroups, setTaskGroups] = useState([]); - const [loading, setLoading] = useState(true); + const [projects, setProjects] = useState([]); const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [retryTimeout, setRetryTimeout] = useState(null); - const router = useRouter(); - const { data: session } = useSession(); - const fetchTasks = useCallback(async (isRefresh = false, retryCount = 0) => { + const fetchStatusLabels = async (isRefresh = false) => { try { if (isRefresh) { setRefreshing(true); } - - if (retryTimeout) { - clearTimeout(retryTimeout); - setRetryTimeout(null); - } - - const response = await fetch('/api/leantime/tasks', { - cache: 'no-store', - next: { revalidate: 0 }, - }); + const response = await fetch('/api/leantime/status-labels'); - 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); + const retryAfter = parseInt(response.headers.get('retry-after') || '60'); + const timeout = setTimeout(() => fetchStatusLabels(), retryAfter * 1000); setRetryTimeout(timeout); + setError(`Rate limit exceeded. Retrying in ${retryAfter} seconds...`); return; } if (!response.ok) { - throw new Error('Failed to fetch tasks'); + throw new Error('Failed to fetch status labels'); } 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); + setProjects(data.projects || []); setError(null); } catch (err) { - console.error('Error fetching tasks:', err); - setError(err instanceof Error ? err.message : 'Failed to fetch tasks'); + console.error('Error fetching status labels:', err); + setError('Failed to fetch status labels'); } finally { setLoading(false); setRefreshing(false); } - }, [retryTimeout]); + }; useEffect(() => { - if (session) { - fetchTasks(); - const interval = setInterval(() => fetchTasks(), 300000); - return () => { - clearInterval(interval); - if (retryTimeout) { - clearTimeout(retryTimeout); - } - }; + fetchStatusLabels(); + return () => { + if (retryTimeout) { + clearTimeout(retryTimeout); + } + }; + }, []); + + const getStatusClass = (className: string) => { + switch (className) { + case 'label-info': + case 'label-blue': + return 'text-blue-600'; + case 'label-warning': + return 'text-yellow-600'; + case 'label-success': + return 'text-green-600'; + case 'label-dark-green': + return 'text-emerald-600'; + case 'label-important': + return 'text-red-600'; + case 'label-default': + return 'text-gray-600'; + default: + return 'text-gray-600'; } - }, [session, fetchTasks, retryTimeout]); + }; return ( - router.push('/flow')} - > + - - 📋 My ToDos - -
- -
+ Flow +
- - {loading &&

Loading tasks...

} - {error && ( -
-

Error: {error}

- {!retryTimeout && ( - - )} + + {loading ? ( +
+
- )} - {!loading && !error && ( + ) : error ? ( +
{error}
+ ) : projects.length === 0 ? ( +
No status labels found
+ ) : (
-
- - -
- {taskGroups.length === 0 ? ( -

No tasks found

- ) : ( - taskGroups.map((group) => ( -
-
- - {group.name === 'overdue' && 🔥} -

- {group.name.charAt(0).toUpperCase() + group.name.slice(1)} ({group.count}) -

-
-
- {group.tasks.map((task) => ( -
- {group.name === 'overdue' && ( -
- )} -
-
{task.projectName}
-
- {task.headline} - {task.details && ( - // {task.details} - )} -
-
- 🗓️ {new Date(task.dueDate).toLocaleDateString()} -
-
-
- ))} -
+ {projects.map((project) => ( +
+

Project {project.projectId}

+
+ {project.labels.map((label, index) => ( +
+ {label.name} + + {label.statusType} + +
+ ))}
- )) - )} +
+ ))}
)}