Switch to tags
Some checks failed
Docker Deploy / build-and-push (push) Has been cancelled

This commit is contained in:
2026-01-20 11:09:09 -07:00
parent de5b1063b7
commit ad7dc18780
22 changed files with 1516 additions and 1057 deletions

View File

@@ -1,72 +1,96 @@
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';
import type { APIRoute } from "astro";
import { db } from "../../../db";
import {
timeEntries,
members,
users,
clients,
tags,
timeEntryTags,
} from "../../../db/schema";
import { eq, and, gte, lte, desc, inArray } from "drizzle-orm";
export const GET: APIRoute = async ({ request, locals, cookies }) => {
const user = locals.user;
if (!user) {
return new Response('Unauthorized', { status: 401 });
return new Response("Unauthorized", { status: 401 });
}
// Get current team from cookie
const currentTeamId = cookies.get('currentTeamId')?.value;
const currentTeamId = cookies.get("currentTeamId")?.value;
const userMemberships = await db.select()
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 });
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.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 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':
case "today":
startDate.setHours(0, 0, 0, 0);
endDate.setHours(23, 59, 59, 999);
break;
case 'week':
case "week":
startDate.setDate(now.getDate() - 7);
break;
case 'month':
case "month":
startDate.setMonth(now.getMonth() - 1);
break;
case 'mtd':
case "mtd":
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
break;
case 'ytd':
case "ytd":
startDate = new Date(now.getFullYear(), 0, 1);
break;
case 'last-month':
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':
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);
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);
const parts = customTo.split("-");
endDate = new Date(
parseInt(parts[0]),
parseInt(parts[1]) - 1,
parseInt(parts[2]),
23,
59,
59,
999,
);
}
break;
}
@@ -81,31 +105,58 @@ export const GET: APIRoute = async ({ request, locals, cookies }) => {
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,
})
const entries = await db
.select({
entry: timeEntries,
user: users,
client: clients,
})
.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();
// Fetch tags for these entries
const entryIds = entries.map((e) => e.entry.id);
const tagsMap = new Map<string, string[]>();
if (entryIds.length > 0) {
const entryTags = await db
.select({
entryId: timeEntryTags.timeEntryId,
tagName: tags.name,
})
.from(timeEntryTags)
.innerJoin(tags, eq(timeEntryTags.tagId, tags.id))
.where(inArray(timeEntryTags.timeEntryId, entryIds))
.all();
for (const tag of entryTags) {
if (!tagsMap.has(tag.entryId)) {
tagsMap.set(tag.entryId, []);
}
tagsMap.get(tag.entryId)!.push(tag.tagName);
}
}
// Generate CSV
const headers = ['Date', 'Start Time', 'End Time', 'Duration (h)', 'Member', 'Client', 'Category', 'Description'];
const rows = entries.map(e => {
const headers = [
"Date",
"Start Time",
"End Time",
"Duration (h)",
"Member",
"Client",
"Tags",
"Description",
];
const rows = entries.map((e) => {
const start = e.entry.startTime;
const end = e.entry.endTime;
@@ -114,24 +165,26 @@ export const GET: APIRoute = async ({ request, locals, cookies }) => {
duration = (end.getTime() - start.getTime()) / (1000 * 60 * 60); // Hours
}
const tagsStr = tagsMap.get(e.entry.id)?.join("; ") || "";
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(',');
end ? end.toLocaleTimeString() : "",
end ? duration.toFixed(2) : "Running",
`"${(e.user.name || "").replace(/"/g, '""')}"`,
`"${(e.client.name || "").replace(/"/g, '""')}"`,
`"${tagsStr.replace(/"/g, '""')}"`,
`"${(e.entry.description || "").replace(/"/g, '""')}"`,
].join(",");
});
const csvContent = [headers.join(','), ...rows].join('\n');
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"`,
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="time-entries-${startDate.toISOString().split("T")[0]}-to-${endDate.toISOString().split("T")[0]}.csv"`,
},
});
};