import type { APIRoute } from "astro"; import { db } from "../../../db"; import { timeEntries, members, users, clients, tags } from "../../../db/schema"; import { eq, and, gte, lte, desc } from "drizzle-orm"; export const GET: APIRoute = async ({ request, locals, cookies }) => { const user = locals.user; if (!user) { return new Response("Unauthorized", { status: 401 }); } // Get current team from cookie const currentTeamId = cookies.get("currentTeamId")?.value; const userMemberships = await db .select() .from(members) .where(eq(members.userId, user.id)) .all(); if (userMemberships.length === 0) { return new Response("No organization found", { status: 404 }); } // Use current team or fallback to first membership const userMembership = currentTeamId ? userMemberships.find((m) => m.organizationId === currentTeamId) || userMemberships[0] : userMemberships[0]; const url = new URL(request.url); const selectedMemberId = url.searchParams.get("member") || ""; const selectedClientId = url.searchParams.get("client") || ""; const timeRange = url.searchParams.get("range") || "week"; const customFrom = url.searchParams.get("from"); const customTo = url.searchParams.get("to"); const now = new Date(); let startDate = new Date(); let endDate = new Date(); switch (timeRange) { case "today": startDate.setHours(0, 0, 0, 0); endDate.setHours(23, 59, 59, 999); break; case "week": startDate.setDate(now.getDate() - 7); break; case "month": startDate.setMonth(now.getMonth() - 1); break; case "mtd": startDate = new Date(now.getFullYear(), now.getMonth(), 1); break; case "ytd": startDate = new Date(now.getFullYear(), 0, 1); break; case "last-month": startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1); endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); break; case "custom": if (customFrom) { const parts = customFrom.split("-"); startDate = new Date( parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 0, 0, 0, 0, ); } if (customTo) { const parts = customTo.split("-"); endDate = new Date( parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 23, 59, 59, 999, ); } break; } const conditions = [ eq(timeEntries.organizationId, userMembership.organizationId), gte(timeEntries.startTime, startDate), lte(timeEntries.startTime, endDate), ]; if (selectedMemberId) { conditions.push(eq(timeEntries.userId, selectedMemberId)); } if (selectedClientId) { conditions.push(eq(timeEntries.clientId, selectedClientId)); } const entries = await db .select({ entry: timeEntries, user: users, client: clients, tag: tags, }) .from(timeEntries) .innerJoin(users, eq(timeEntries.userId, users.id)) .innerJoin(clients, eq(timeEntries.clientId, clients.id)) .leftJoin(tags, eq(timeEntries.tagId, tags.id)) .where(and(...conditions)) .orderBy(desc(timeEntries.startTime)) .all(); // Generate CSV const headers = [ "Date", "Start Time", "End Time", "Duration (h)", "Member", "Client", "Tag", "Description", ]; const sanitizeCell = (value: string): string => { if (/^[=+\-@\t\r]/.test(value)) { return `\t${value}`; } return value; }; const rows = entries.map((e) => { const start = e.entry.startTime; const end = e.entry.endTime; let duration = 0; if (end) { duration = (end.getTime() - start.getTime()) / (1000 * 60 * 60); // Hours } const tagsStr = e.tag?.name || ""; return [ start.toLocaleDateString(), start.toLocaleTimeString(), end ? end.toLocaleTimeString() : "", end ? duration.toFixed(2) : "Running", `"${sanitizeCell((e.user.name || "").replace(/"/g, '""'))}"`, `"${sanitizeCell((e.client.name || "").replace(/"/g, '""'))}"`, `"${sanitizeCell(tagsStr.replace(/"/g, '""'))}"`, `"${sanitizeCell((e.entry.description || "").replace(/"/g, '""'))}"`, ].join(","); }); const csvContent = [headers.join(","), ...rows].join("\n"); return new Response(csvContent, { headers: { "Content-Type": "text/csv", "Content-Disposition": `attachment; filename="time-entries-${startDate.toISOString().split("T")[0]}-to-${endDate.toISOString().split("T")[0]}.csv"`, }, }); };