Files
chronus/src/pages/api/reports/export.ts
Atridad Lahiji bf2a1816db
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m59s
Added custom ranges for report filtering + CSV exports
2026-01-19 15:18:34 -07:00

138 lines
4.4 KiB
TypeScript

import type { APIRoute } from 'astro';
import { db } from '../../../db';
import { timeEntries, members, users, clients, categories } 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 selectedCategoryId = url.searchParams.get('category') || '';
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 (selectedCategoryId) {
conditions.push(eq(timeEntries.categoryId, selectedCategoryId));
}
if (selectedClientId) {
conditions.push(eq(timeEntries.clientId, selectedClientId));
}
const entries = await db.select({
entry: timeEntries,
user: users,
client: clients,
category: categories,
})
.from(timeEntries)
.innerJoin(users, eq(timeEntries.userId, users.id))
.innerJoin(clients, eq(timeEntries.clientId, clients.id))
.innerJoin(categories, eq(timeEntries.categoryId, categories.id))
.where(and(...conditions))
.orderBy(desc(timeEntries.startTime))
.all();
// Generate CSV
const headers = ['Date', 'Start Time', 'End Time', 'Duration (h)', 'Member', 'Client', 'Category', 'Description'];
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
}
return [
start.toLocaleDateString(),
start.toLocaleTimeString(),
end ? end.toLocaleTimeString() : '',
end ? duration.toFixed(2) : 'Running',
`"${(e.user.name || '').replace(/"/g, '""')}"`,
`"${(e.client.name || '').replace(/"/g, '""')}"`,
`"${(e.category.name || '').replace(/"/g, '""')}"`,
`"${(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"`,
},
});
};