198 lines
4.9 KiB
TypeScript
198 lines
4.9 KiB
TypeScript
import { FastifyInstance } from "fastify"
|
|
import { db } from "../db"
|
|
import { projects, timeEntries, users } from "../db/schema"
|
|
import { eq, and, sql, inArray } from "drizzle-orm"
|
|
import { z } from "zod"
|
|
|
|
const ProjectSchema = z.object({
|
|
name: z.string().min(1),
|
|
customerId: z.string().uuid()
|
|
})
|
|
|
|
const ProjectUpdateSchema = ProjectSchema.partial()
|
|
|
|
const ProjectCloneSchema = z.object({
|
|
name: z.string().optional()
|
|
})
|
|
|
|
const BulkRenameSchema = z.object({
|
|
ids: z.array(z.string().uuid()).min(1),
|
|
prefix: z.string()
|
|
})
|
|
|
|
const BulkDeleteSchema = z.object({
|
|
ids: z.array(z.string().uuid()).min(1)
|
|
})
|
|
|
|
export default async function projectRoutes(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 { onlyActive, customerId } = request.query as { onlyActive?: string, customerId?: string }
|
|
const isActiveDefault = onlyActive !== "false"
|
|
|
|
const conds = []
|
|
if (isActiveDefault) conds.push(eq(projects.active, true))
|
|
if (customerId) conds.push(eq(projects.customerId, customerId))
|
|
|
|
const results = await db
|
|
.select()
|
|
.from(projects)
|
|
.where(conds.length ? and(...conds) : undefined as any)
|
|
.orderBy(projects.name)
|
|
|
|
return results
|
|
})
|
|
|
|
fastify.get("/:id", async (request, reply) => {
|
|
const { id } = request.params as { id: string }
|
|
|
|
const [project] = await db
|
|
.select()
|
|
.from(projects)
|
|
.where(eq(projects.id, id))
|
|
.limit(1)
|
|
|
|
if (!project) {
|
|
return reply.code(404).send({ message: "Project not found" })
|
|
}
|
|
|
|
return project
|
|
})
|
|
|
|
fastify.get("/:id/stats", async (request, reply) => {
|
|
const { id } = request.params as { id: string }
|
|
|
|
const projectExists = await db
|
|
.select({ id: projects.id })
|
|
.from(projects)
|
|
.where(eq(projects.id, id))
|
|
.limit(1)
|
|
|
|
if (!projectExists.length) {
|
|
return reply.code(404).send({ message: "Project not found" })
|
|
}
|
|
|
|
const totalStats = await db
|
|
.select({
|
|
totalHours: sql<number>`sum(hours)`,
|
|
entryCount: sql<number>`count(*)`,
|
|
})
|
|
.from(timeEntries)
|
|
.where(eq(timeEntries.projectId, id))
|
|
|
|
const byUser = await db
|
|
.select({
|
|
userId: users.id,
|
|
name: users.name,
|
|
hours: sql<number>`sum(hours)`,
|
|
})
|
|
.from(timeEntries)
|
|
.innerJoin(users, eq(timeEntries.userId, users.id))
|
|
.where(eq(timeEntries.projectId, id))
|
|
.groupBy(users.id, users.name)
|
|
|
|
const byMonth = await db
|
|
.select({
|
|
month: sql`date_trunc('month', date)`,
|
|
hours: sql<number>`sum(hours)`,
|
|
})
|
|
.from(timeEntries)
|
|
.where(eq(timeEntries.projectId, id))
|
|
.groupBy(sql`date_trunc('month', date)`)
|
|
.orderBy(sql`date_trunc('month', date)`)
|
|
|
|
return {
|
|
totalHours: totalStats[0]?.totalHours || 0,
|
|
entryCount: totalStats[0]?.entryCount || 0,
|
|
byUser,
|
|
byMonth,
|
|
}
|
|
})
|
|
|
|
fastify.post("/", async (request, reply) => {
|
|
const body = ProjectSchema.parse(request.body)
|
|
|
|
const [project] = await db
|
|
.insert(projects)
|
|
.values({
|
|
name: body.name,
|
|
customerId: body.customerId
|
|
})
|
|
.returning()
|
|
|
|
return reply.code(201).send(project)
|
|
})
|
|
|
|
fastify.post("/:id/clone", async (request, reply) => {
|
|
const { id } = request.params as { id: string }
|
|
const body = ProjectCloneSchema.parse(request.body || {})
|
|
|
|
const [original] = await db
|
|
.select()
|
|
.from(projects)
|
|
.where(eq(projects.id, id))
|
|
.limit(1)
|
|
|
|
if (!original) {
|
|
return reply.code(404).send({ message: "Project not found" })
|
|
}
|
|
|
|
const [cloned] = await db
|
|
.insert(projects)
|
|
.values({
|
|
name: body.name || `${original.name} (Kopie)`,
|
|
customerId: original.customerId,
|
|
active: original.active
|
|
})
|
|
.returning()
|
|
|
|
return reply.code(201).send(cloned)
|
|
})
|
|
|
|
fastify.patch("/:id", async (request, reply) => {
|
|
const { id } = request.params as { id: string }
|
|
const body = ProjectUpdateSchema.parse(request.body)
|
|
|
|
const [project] = await db
|
|
.update(projects)
|
|
.set(body)
|
|
.where(eq(projects.id, id))
|
|
.returning()
|
|
|
|
if (!project) {
|
|
return reply.code(404).send({ message: "Project not found" })
|
|
}
|
|
|
|
return project
|
|
})
|
|
|
|
fastify.post("/bulk-rename", async (request, reply) => {
|
|
const { ids, prefix } = BulkRenameSchema.parse(request.body)
|
|
|
|
await db
|
|
.update(projects)
|
|
.set({
|
|
name: sql`${prefix} ${projects.name}`
|
|
})
|
|
.where(inArray(projects.id, ids))
|
|
|
|
return { success: true }
|
|
})
|
|
|
|
fastify.post("/bulk-delete", async (request, reply) => {
|
|
const { ids } = BulkDeleteSchema.parse(request.body)
|
|
|
|
await db
|
|
.delete(projects)
|
|
.where(inArray(projects.id, ids))
|
|
|
|
return { success: true }
|
|
})
|
|
} |