EmberClone/apps/api/src/routes/customers.ts

208 lines
5.3 KiB
TypeScript

import { FastifyInstance } from "fastify"
import { db } from "../db"
import { customers, projects, timeEntries } from "../db/schema"
import { eq, and, inArray, desc } from "drizzle-orm"
import { z } from "zod"
const CustomerSchema = z.object({
name: z.string().min(1)
})
const CustomerUpdateSchema = CustomerSchema.partial()
const MergeSchema = z.object({
targetId: z.string().uuid()
})
export default async function customerRoutes(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 } = request.query as { onlyActive?: string }
const isActiveDefault = onlyActive !== "false"
const results = await db
.select()
.from(customers)
.where(isActiveDefault ? eq(customers.active, true) : undefined)
.orderBy(customers.name)
return results
})
fastify.get("/:id", async (request, reply) => {
const { id } = request.params as { id: string }
const [customer] = await db
.select()
.from(customers)
.where(eq(customers.id, id))
.limit(1)
if (!customer) {
return reply.code(404).send({ message: "Customer not found" })
}
return customer
})
fastify.get("/:id/projects", async (request, reply) => {
const { id } = request.params as { id: string }
const results = await db
.select()
.from(projects)
.where(eq(projects.customerId, id))
.orderBy(projects.name)
return results
})
fastify.get("/:id/time-entries", async (request, reply) => {
const { id } = request.params as { id: string }
const customerProjects = await db
.select({ id: projects.id })
.from(projects)
.where(eq(projects.customerId, id))
if (customerProjects.length === 0) {
return []
}
const projectIds = customerProjects.map((p) => p.id)
const results = await db
.select()
.from(timeEntries)
.where(inArray(timeEntries.projectId, projectIds))
.orderBy(desc(timeEntries.startTime))
.limit(50)
return results
})
fastify.post("/", async (request, reply) => {
const body = CustomerSchema.parse(request.body)
const [customer] = await db
.insert(customers)
.values({
name: body.name
})
.returning()
return reply.code(201).send(customer)
})
fastify.patch("/:id", async (request, reply) => {
const { id } = request.params as { id: string }
const body = CustomerUpdateSchema.parse(request.body)
const [customer] = await db
.update(customers)
.set(body)
.where(eq(customers.id, id))
.returning()
if (!customer) {
return reply.code(404).send({ message: "Customer not found" })
}
return customer
})
fastify.delete("/:id", async (request, reply) => {
const { id } = request.params as { id: string }
const [customer] = await db
.update(customers)
.set({ active: false })
.where(eq(customers.id, id))
.returning()
if (!customer) {
return reply.code(404).send({ message: "Customer not found" })
}
return reply.code(204).send()
})
fastify.post("/import", async (request, reply) => {
const user = request.user as any
if (user?.role !== "admin") {
return reply.code(403).send({ message: "Admin privileges required" })
}
const data = await request.file()
if (!data) {
return reply.code(400).send({ message: "No file uploaded" })
}
const content = await data.toBuffer()
const csvText = content.toString("utf-8")
const lines = csvText.split(/\r?\n/).filter(line => line.trim())
const rows = lines.slice(1)
let imported = 0
const errors: string[] = []
for (let i = 0; i < rows.length; i++) {
try {
const [name, activeStr] = rows[i].split(",").map(s => s.trim())
if (!name) throw new Error("Name is required")
await db.insert(customers).values({
name,
active: activeStr === "true"
})
imported++
} catch (e: any) {
errors.push(`Row ${i + 2}: ${e.message}`)
}
}
return { imported, errors }
})
fastify.post("/:id/merge", async (request, reply) => {
const user = request.user as any
if (user?.role !== "admin") {
return reply.code(403).send({ message: "Admin privileges required" })
}
const { id: sourceId } = request.params as { id: string }
const { targetId } = MergeSchema.parse(request.body)
if (sourceId === targetId) {
return reply.code(400).send({ message: "Source and target customer must be different" })
}
await db.transaction(async (tx) => {
// Move all projects from source to target
await tx
.update(projects)
.set({ customerId: targetId })
.where(eq(projects.customerId, sourceId))
// Deactivate source customer
const [customer] = await tx
.update(customers)
.set({ active: false })
.where(eq(customers.id, sourceId))
.returning()
if (!customer) {
throw new Error("Source customer not found")
}
})
return reply.code(200).send({ message: "Customers merged successfully" })
})
}