First pass

This commit is contained in:
2025-12-25 22:10:06 -07:00
parent a2af6195f9
commit 455c3dbd9a
58 changed files with 10299 additions and 3 deletions

View File

@@ -0,0 +1,36 @@
import type { APIRoute } from 'astro';
import { db } from '../../../../db';
import { timeEntries } from '../../../../db/schema';
import { eq, and } from 'drizzle-orm';
export const POST: APIRoute = async ({ params, locals, redirect }) => {
const user = locals.user;
if (!user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
}
const entryId = params.id;
if (!entryId) {
return new Response(JSON.stringify({ error: 'Entry ID required' }), { status: 400 });
}
// Verify the entry belongs to the user
const entry = await db.select()
.from(timeEntries)
.where(and(
eq(timeEntries.id, entryId),
eq(timeEntries.userId, user.id)
))
.get();
if (!entry) {
return new Response(JSON.stringify({ error: 'Entry not found' }), { status: 404 });
}
// Delete the entry
await db.delete(timeEntries)
.where(eq(timeEntries.id, entryId))
.run();
return redirect('/dashboard/tracker');
};

View File

@@ -0,0 +1,78 @@
import type { APIRoute } from 'astro';
import { db } from '../../../db';
import { timeEntries, members, timeEntryTags, categories } from '../../../db/schema';
import { eq, and, isNull } from 'drizzle-orm';
import { nanoid } from 'nanoid';
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
const body = await request.json();
const description = body.description || '';
const clientId = body.clientId;
const categoryId = body.categoryId;
const tags = body.tags || [];
if (!clientId) {
return new Response('Client is required', { status: 400 });
}
if (!categoryId) {
return new Response('Category is required', { status: 400 });
}
// Check for running entry
const runningEntry = await db.select().from(timeEntries).where(
and(
eq(timeEntries.userId, locals.user.id),
isNull(timeEntries.endTime)
)
).get();
if (runningEntry) {
return new Response('Timer already running', { status: 400 });
}
// Get default org (first one)
const member = await db.select().from(members).where(eq(members.userId, locals.user.id)).limit(1).get();
if (!member) {
return new Response('No organization found', { status: 400 });
}
// Verify category belongs to user's organization
const category = await db.select().from(categories).where(
and(
eq(categories.id, categoryId),
eq(categories.organizationId, member.organizationId)
)
).get();
if (!category) {
return new Response('Invalid category', { status: 400 });
}
const startTime = new Date();
const id = nanoid();
await db.insert(timeEntries).values({
id,
userId: locals.user.id,
organizationId: member.organizationId,
clientId,
categoryId,
startTime,
description,
});
// Add tags if provided
if (tags.length > 0) {
await db.insert(timeEntryTags).values(
tags.map((tagId: string) => ({
timeEntryId: id,
tagId,
}))
);
}
return new Response(JSON.stringify({ id, startTime }), { status: 200 });
};

View File

@@ -0,0 +1,25 @@
import type { APIRoute } from 'astro';
import { db } from '../../../db';
import { timeEntries } from '../../../db/schema';
import { eq, and, isNull } from 'drizzle-orm';
export const POST: APIRoute = async ({ locals }) => {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
const runningEntry = await db.select().from(timeEntries).where(
and(
eq(timeEntries.userId, locals.user.id),
isNull(timeEntries.endTime)
)
).get();
if (!runningEntry) {
return new Response('No timer running', { status: 400 });
}
await db.update(timeEntries)
.set({ endTime: new Date() })
.where(eq(timeEntries.id, runningEntry.id));
return new Response(JSON.stringify({ success: true }), { status: 200 });
};