diff --git a/apps/api/src/routes/time-entries.ts b/apps/api/src/routes/time-entries.ts new file mode 100644 index 0000000..439e30f --- /dev/null +++ b/apps/api/src/routes/time-entries.ts @@ -0,0 +1,147 @@ +import { FastifyPluginAsync } from "fastify" +import { db } from "../db" +import { timeEntries } from "../db/schema" +import { eq, and, gte, lte, or } from "drizzle-orm" +import { z } from "zod" + +const TimeEntrySchema = z.object({ + projectId: z.string().uuid().optional(), + description: z.string().min(1), + startTime: z.string().datetime(), + endTime: z.string().datetime().optional() +}) + +const TimeEntryUpdateSchema = TimeEntrySchema.partial() + +export default async function timeEntryRoutes(fastify: FastifyPluginAsync) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + fastify.get("/", async (request, reply) => { + const { from, to } = request.query as { from?: string; to?: string } + const user = request.user as { sub: string; role: string } + + const filters = [] + + if (user.role !== "admin") { + filters.push(eq(timeEntries.userId, user.sub)) + } + + if (from) { + filters.push(gte(timeEntries.startTime, new Date(from))) + } + + if (to) { + filters.push(lte(timeEntries.startTime, new Date(to))) + } + + const entries = await db + .select() + .from(timeEntries) + .where(and(...filters)) + .orderBy(timeEntries.startTime) + + return entries + }) + + fastify.get("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + const user = request.user as { sub: string; role: string } + + const [entry] = await db + .select() + .from(timeEntries) + .where( + and( + eq(timeEntries.id, id), + user.role === "admin" ? undefined : eq(timeEntries.userId, user.sub) + ) + ) + .limit(1) + + if (!entry) { + return reply.code(404).send({ message: "Time entry not found" }) + } + + return entry + }) + + fastify.post("/", async (request, reply) => { + const user = request.user as { sub: string } + const body = TimeEntrySchema.parse(request.body) + + const [entry] = await db + .insert(timeEntries) + .values({ + ...body, + userId: user.sub, + startTime: new Date(body.startTime), + endTime: body.endTime ? new Date(body.endTime) : null + }) + .returning() + + return reply.code(201).send(entry) + }) + + fastify.patch("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + const user = request.user as { sub: string; role: string } + const body = TimeEntryUpdateSchema.parse(request.body) + + const [entry] = await db + .select() + .from(timeEntries) + .where( + and( + eq(timeEntries.id, id), + user.role === "admin" ? undefined : eq(timeEntries.userId, user.sub) + ) + ) + .limit(1) + + if (!entry) { + return reply.code(404).send({ message: "Time entry not found" }) + } + + const updateData: any = { ...body } + if (body.startTime) updateData.startTime = new Date(body.startTime) + if (body.endTime) updateData.endTime = body.endTime ? new Date(body.endTime) : null + + const [updated] = await db + .update(timeEntries) + .set(updateData) + .where(eq(timeEntries.id, id)) + .returning() + + return updated + }) + + fastify.delete("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + const user = request.user as { sub: string; role: string } + + const [entry] = await db + .select() + .from(timeEntries) + .where( + and( + eq(timeEntries.id, id), + user.role === "admin" ? undefined : eq(timeEntries.userId, user.sub) + ) + ) + .limit(1) + + if (!entry) { + return reply.code(404).send({ message: "Time entry not found" }) + } + + await db.delete(timeEntries).where(eq(timeEntries.id, id)) + + return reply.code(204).send() + }) +} \ No newline at end of file