Files
chronus/src/pages/api/reports/export.ts
Atridad Lahiji 12d59bb42f
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m57s
Refactored a bunch of shit
2026-02-09 01:49:19 -07:00

170 lines
4.6 KiB
TypeScript

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