208 lines
5.3 KiB
TypeScript
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" })
|
|
})
|
|
} |