From c5045905c44c99487ddded49730c9f751da70a2b Mon Sep 17 00:00:00 2001 From: Alma Date: Sat, 12 Apr 2025 13:24:16 +0200 Subject: [PATCH] working leantime widget 14 --- app/api/leantime/status-labels/route.ts | 108 +++++++++++++---------- components/flow.tsx | 112 ++++++++++++++---------- lib/utils.ts | 19 +++- 3 files changed, 141 insertions(+), 98 deletions(-) diff --git a/app/api/leantime/status-labels/route.ts b/app/api/leantime/status-labels/route.ts index ebd83290..86b7e2a8 100644 --- a/app/api/leantime/status-labels/route.ts +++ b/app/api/leantime/status-labels/route.ts @@ -2,23 +2,18 @@ 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 Task { + id: string; + headline: string; + projectName: string; + status: string; + dueDate: string | null; + details?: string; + milestone?: string; } -interface Project { - projectId: string; - labels: StatusLabel[]; -} - -// Simple in-memory cache for user IDs and status labels +// Cache for user IDs const userCache = new Map(); -const statusLabelsCache = new Map(); -const CACHE_TTL = 60000; // 1 minute cache TTL export async function GET() { const session = await getServerSession(authOptions); @@ -28,7 +23,7 @@ export async function GET() { } try { - console.log('Fetching status labels for user:', session.user.id); + console.log('Fetching tasks for user:', session.user.id); console.log('Using LEANTIME_TOKEN:', process.env.LEANTIME_TOKEN ? 'Present' : 'Missing'); // Check cache first @@ -85,13 +80,7 @@ export async function GET() { } } - // Check status labels cache - const cachedLabels = statusLabelsCache.get(leantimeUserId); - if (cachedLabels && (Date.now() - cachedLabels.timestamp) < CACHE_TTL) { - return NextResponse.json({ projects: cachedLabels.data }); - } - - // Now fetch the status labels using leantimeUserId + // Fetch all tasks assigned to the user const response = await fetch('https://agilite.slm-lab.net/api/jsonrpc', { method: 'POST', headers: { @@ -100,57 +89,80 @@ export async function GET() { }, body: JSON.stringify({ jsonrpc: '2.0', - method: 'leantime.rpc.Tickets.Tickets.getAllStatusLabelsByUserId', + method: 'leantime.rpc.Tickets.Tickets.getAllByUserId', id: 1, params: { - userId: leantimeUserId // Use the Leantime user ID here + userId: leantimeUserId, + status: "all", + limit: 100 } }) }); if (!response.ok) { const errorData = await response.json(); - console.error('Status labels fetch failed:', errorData); - throw new Error(`Failed to fetch status labels: ${errorData.error || 'Unknown error'}`); + console.error('Tasks fetch failed:', errorData); + throw new Error(`Failed to fetch tasks: ${errorData.error || 'Unknown error'}`); } const data = await response.json(); - console.log('Status labels response:', data); + console.log('Tasks response:', data); if (!data.result) { - return NextResponse.json({ projects: [] }); + return NextResponse.json({ tasks: [] }); } - // 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, - class: label.class, - statusType: label.statusType, - kanbanCol: label.kanbanCol, - sortKey: Number(label.sortKey) || 0 - })); + // Get project details to include project names + const projectsResponse = await fetch('https://agilite.slm-lab.net/api/jsonrpc', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': process.env.LEANTIME_TOKEN || '', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.Projects.getAll', + id: 1 + }) + }); - // Sort labels by sortKey - labels.sort((a, b) => a.sortKey - b.sortKey); + if (!projectsResponse.ok) { + throw new Error('Failed to fetch projects'); + } + const projectsData = await projectsResponse.json(); + const projectsMap = new Map( + projectsData.result.map((project: any) => [project.id, project.name]) + ); + + // Transform and categorize the tasks + const tasks = data.result.map((task: any) => { + const dueDate = task.dateToFinish ? new Date(task.dateToFinish * 1000) : null; + return { - projectId, - labels + id: task.id, + headline: task.headline, + projectName: projectsMap.get(task.projectId) || `Project ${task.projectId}`, + status: task.status, + dueDate: dueDate ? dueDate.toISOString() : null, + details: task.description, + milestone: task.milestoneName }; }); - // Cache the transformed data - statusLabelsCache.set(leantimeUserId, { - data: projects, - timestamp: Date.now() + // Sort tasks by due date + tasks.sort((a: Task, b: Task) => { + if (!a.dueDate && !b.dueDate) return 0; + if (!a.dueDate) return 1; + if (!b.dueDate) return -1; + return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(); }); - return NextResponse.json({ projects }); + return NextResponse.json({ tasks }); } catch (error) { - console.error('Error fetching status labels:', error); + console.error('Error fetching tasks:', error); return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to fetch status labels" }, + { error: error instanceof Error ? error.message : "Failed to fetch tasks" }, { status: 500 } ); } diff --git a/components/flow.tsx b/components/flow.tsx index d951b038..df1b848e 100644 --- a/components/flow.tsx +++ b/components/flow.tsx @@ -4,28 +4,26 @@ import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { RefreshCw } from "lucide-react"; +import { formatDate } from "@/lib/utils"; -interface StatusLabel { - name: string; - class: string; - statusType: string; - kanbanCol: boolean | string; - sortKey: number; -} - -interface Project { - projectId: string; - labels: StatusLabel[]; +interface Task { + id: string; + headline: string; + projectName: string; + status: string; + dueDate: string | null; + details?: string; + milestone?: string; } export function Flow() { - const [projects, setProjects] = useState([]); + const [tasks, setTasks] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [retryTimeout, setRetryTimeout] = useState(null); - const fetchStatusLabels = async (isRefresh = false) => { + const fetchTasks = async (isRefresh = false) => { try { if (isRefresh) { setRefreshing(true); @@ -34,22 +32,22 @@ export function Flow() { if (response.status === 429) { const retryAfter = parseInt(response.headers.get('retry-after') || '60'); - const timeout = setTimeout(() => fetchStatusLabels(), retryAfter * 1000); + const timeout = setTimeout(() => fetchTasks(), retryAfter * 1000); setRetryTimeout(timeout); setError(`Rate limit exceeded. Retrying in ${retryAfter} seconds...`); return; } if (!response.ok) { - throw new Error('Failed to fetch status labels'); + throw new Error('Failed to fetch tasks'); } const data = await response.json(); - setProjects(data.projects || []); + setTasks(data.tasks || []); setError(null); } catch (err) { - console.error('Error fetching status labels:', err); - setError('Failed to fetch status labels'); + console.error('Error fetching tasks:', err); + setError('Failed to fetch tasks'); } finally { setLoading(false); setRefreshing(false); @@ -57,7 +55,7 @@ export function Flow() { }; useEffect(() => { - fetchStatusLabels(); + fetchTasks(); return () => { if (retryTimeout) { clearTimeout(retryTimeout); @@ -65,26 +63,34 @@ export function Flow() { }; }, []); - const getStatusClass = (className: string) => { - switch (className) { - case 'label-info': - case 'label-blue': + const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case 'new': 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': + case 'blocked': return 'text-red-600'; - case 'label-default': - return 'text-gray-600'; + case 'in progress': + case 'inprogress': + return 'text-orange-600'; + case 'done': + case 'archived': + return 'text-green-600'; + case 'waiting for approval': + return 'text-yellow-600'; default: return 'text-gray-600'; } }; + // Group tasks by project + const groupedTasks = tasks.reduce((acc, task) => { + if (!acc[task.projectName]) { + acc[task.projectName] = []; + } + acc[task.projectName].push(task); + return acc; + }, {} as Record); + return ( @@ -92,7 +98,7 @@ export function Flow() {