287 lines
7.6 KiB
TypeScript
287 lines
7.6 KiB
TypeScript
import { FastifyInstance } from "fastify"
|
|
import { db } from "../db"
|
|
import { timeEntries, timeEntryAttachments } from "../db/schema"
|
|
import { eq, and, gte, lte, isNull, inArray } from "drizzle-orm"
|
|
import { z } from "zod"
|
|
import { webhookDispatcher } from "../services/webhooks"
|
|
|
|
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()
|
|
|
|
const StartEntrySchema = z.object({
|
|
projectId: z.string().uuid().optional(),
|
|
description: z.string().min(1)
|
|
})
|
|
|
|
const BulkDeleteSchema = z.object({
|
|
ids: z.array(z.string().uuid())
|
|
})
|
|
|
|
const AttachmentSchema = z.object({
|
|
documentIds: z.array(z.string().uuid())
|
|
})
|
|
|
|
export default async function timeEntryRoutes(fastify: FastifyInstance) {
|
|
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(filters.length ? and(...filters) : undefined as any)
|
|
.orderBy(timeEntries.startTime)
|
|
|
|
return entries
|
|
})
|
|
|
|
fastify.get("/running", async (request, reply) => {
|
|
const user = request.user as { sub: string }
|
|
|
|
const [entry] = await db
|
|
.select()
|
|
.from(timeEntries)
|
|
.where(
|
|
and(
|
|
eq(timeEntries.userId, user.sub),
|
|
isNull(timeEntries.endTime)
|
|
)
|
|
)
|
|
.limit(1)
|
|
|
|
if (!entry) {
|
|
return reply.code(404).send({ message: "No running time entry found" })
|
|
}
|
|
|
|
return entry
|
|
})
|
|
|
|
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("/start", async (request, reply) => {
|
|
const user = request.user as { sub: string }
|
|
const body = StartEntrySchema.parse(request.body)
|
|
|
|
const [entry] = await db
|
|
.insert(timeEntries)
|
|
.values({
|
|
...body,
|
|
userId: user.sub,
|
|
startTime: new Date(),
|
|
endTime: null
|
|
})
|
|
.returning()
|
|
|
|
await webhookDispatcher.triggerEvent('time_entry.created', entry)
|
|
return reply.code(201).send(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()
|
|
|
|
await webhookDispatcher.triggerEvent('time_entry.created', entry)
|
|
return reply.code(201).send(entry)
|
|
})
|
|
|
|
fastify.post("/:id/stop", 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" })
|
|
}
|
|
|
|
const [updated] = await db
|
|
.update(timeEntries)
|
|
.set({ endTime: new Date() })
|
|
.where(eq(timeEntries.id, id))
|
|
.returning()
|
|
|
|
await webhookDispatcher.triggerEvent('time_entry.updated', updated)
|
|
return updated
|
|
})
|
|
|
|
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 = new Date(body.endTime)
|
|
|
|
const [updated] = await db
|
|
.update(timeEntries)
|
|
.set(updateData)
|
|
.where(eq(timeEntries.id, id))
|
|
.returning()
|
|
|
|
await webhookDispatcher.triggerEvent('time_entry.updated', updated)
|
|
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))
|
|
await webhookDispatcher.triggerEvent('time_entry.deleted', entry)
|
|
return reply.code(204).send()
|
|
})
|
|
|
|
fastify.delete("/bulk", async (request, reply) => {
|
|
const user = request.user as { sub: string; role: string }
|
|
const { ids } = BulkDeleteSchema.parse(request.body)
|
|
|
|
const deletedEntries = await db
|
|
.select()
|
|
.from(timeEntries)
|
|
.where(
|
|
and(
|
|
inArray(timeEntries.id, ids),
|
|
user.role === "admin" ? undefined : eq(timeEntries.userId, user.sub)
|
|
)
|
|
)
|
|
|
|
await db.delete(timeEntries).where(inArray(timeEntries.id, ids))
|
|
|
|
for (const entry of deletedEntries) {
|
|
await webhookDispatcher.triggerEvent('time_entry.deleted', entry)
|
|
}
|
|
|
|
return reply.code(204).send()
|
|
})
|
|
|
|
fastify.post("/:id/attachments", async (request, reply) => {
|
|
const { id } = request.params as { id: string }
|
|
const { documentIds } = AttachmentSchema.parse(request.body)
|
|
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(timeEntryAttachments).where(eq(timeEntryAttachments.timeEntryId, id))
|
|
|
|
const attachments = documentIds.map(docId => ({
|
|
timeEntryId: id,
|
|
documentId: docId
|
|
}))
|
|
|
|
await db.insert(timeEntryAttachments).values(attachments)
|
|
|
|
await webhookDispatcher.triggerEvent('time_entry.updated', entry)
|
|
return reply.code(200).send({ success: true })
|
|
})
|
|
} |