Add repairs domain with tickets, line items, batches, and service templates
Full-stack implementation of instrument repair tracking: DB schema with repair_ticket, repair_line_item, repair_batch, and repair_service_template tables. Backend services and routes with pagination/search/sort. 20 API tests covering CRUD, status workflow, line items, and batch operations. Admin frontend with ticket list, detail with status progression, line item management, batch list/detail with approval workflow, and new ticket form with searchable account picker and intake photo uploads.
This commit is contained in:
209
packages/backend/src/routes/v1/repairs.ts
Normal file
209
packages/backend/src/routes/v1/repairs.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import {
|
||||
PaginationSchema,
|
||||
RepairTicketCreateSchema,
|
||||
RepairTicketUpdateSchema,
|
||||
RepairTicketStatusUpdateSchema,
|
||||
RepairLineItemCreateSchema,
|
||||
RepairLineItemUpdateSchema,
|
||||
RepairBatchCreateSchema,
|
||||
RepairBatchUpdateSchema,
|
||||
RepairBatchStatusUpdateSchema,
|
||||
RepairServiceTemplateCreateSchema,
|
||||
RepairServiceTemplateUpdateSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairServiceTemplateService } from '../../services/repair.service.js'
|
||||
|
||||
export const repairRoutes: FastifyPluginAsync = async (app) => {
|
||||
// --- Repair Tickets ---
|
||||
|
||||
app.post('/repair-tickets', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||
const parsed = RepairTicketCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const ticket = await RepairTicketService.create(app.db, request.companyId, parsed.data)
|
||||
return reply.status(201).send(ticket)
|
||||
})
|
||||
|
||||
app.get('/repair-tickets', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await RepairTicketService.list(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/repair-tickets/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const ticket = await RepairTicketService.getById(app.db, request.companyId, id)
|
||||
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
|
||||
return reply.send(ticket)
|
||||
})
|
||||
|
||||
app.patch('/repair-tickets/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = RepairTicketUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const ticket = await RepairTicketService.update(app.db, request.companyId, id, parsed.data)
|
||||
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
|
||||
return reply.send(ticket)
|
||||
})
|
||||
|
||||
app.post('/repair-tickets/:id/status', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = RepairTicketStatusUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const ticket = await RepairTicketService.updateStatus(app.db, request.companyId, id, parsed.data.status)
|
||||
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
|
||||
return reply.send(ticket)
|
||||
})
|
||||
|
||||
app.delete('/repair-tickets/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const ticket = await RepairTicketService.delete(app.db, request.companyId, id)
|
||||
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
|
||||
return reply.send(ticket)
|
||||
})
|
||||
|
||||
// --- Repair Line Items ---
|
||||
|
||||
app.post('/repair-tickets/:ticketId/line-items', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||
const { ticketId } = request.params as { ticketId: string }
|
||||
const parsed = RepairLineItemCreateSchema.safeParse({ ...(request.body as object), repairTicketId: ticketId })
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const item = await RepairLineItemService.create(app.db, parsed.data)
|
||||
return reply.status(201).send(item)
|
||||
})
|
||||
|
||||
app.get('/repair-tickets/:ticketId/line-items', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
|
||||
const { ticketId } = request.params as { ticketId: string }
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await RepairLineItemService.listByTicket(app.db, ticketId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.patch('/repair-line-items/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = RepairLineItemUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const item = await RepairLineItemService.update(app.db, id, parsed.data)
|
||||
if (!item) return reply.status(404).send({ error: { message: 'Line item not found', statusCode: 404 } })
|
||||
return reply.send(item)
|
||||
})
|
||||
|
||||
app.delete('/repair-line-items/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const item = await RepairLineItemService.delete(app.db, id)
|
||||
if (!item) return reply.status(404).send({ error: { message: 'Line item not found', statusCode: 404 } })
|
||||
return reply.send(item)
|
||||
})
|
||||
|
||||
// --- Repair Batches ---
|
||||
|
||||
app.post('/repair-batches', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||
const parsed = RepairBatchCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const batch = await RepairBatchService.create(app.db, request.companyId, parsed.data)
|
||||
return reply.status(201).send(batch)
|
||||
})
|
||||
|
||||
app.get('/repair-batches', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await RepairBatchService.list(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/repair-batches/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const batch = await RepairBatchService.getById(app.db, request.companyId, id)
|
||||
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
|
||||
return reply.send(batch)
|
||||
})
|
||||
|
||||
app.patch('/repair-batches/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = RepairBatchUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const batch = await RepairBatchService.update(app.db, request.companyId, id, parsed.data)
|
||||
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
|
||||
return reply.send(batch)
|
||||
})
|
||||
|
||||
app.post('/repair-batches/:id/status', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = RepairBatchStatusUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const batch = await RepairBatchService.updateStatus(app.db, request.companyId, id, parsed.data.status)
|
||||
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
|
||||
return reply.send(batch)
|
||||
})
|
||||
|
||||
app.post('/repair-batches/:id/approve', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const batch = await RepairBatchService.approve(app.db, request.companyId, id, request.user.id)
|
||||
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
|
||||
return reply.send(batch)
|
||||
})
|
||||
|
||||
app.post('/repair-batches/:id/reject', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const batch = await RepairBatchService.reject(app.db, request.companyId, id)
|
||||
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
|
||||
return reply.send(batch)
|
||||
})
|
||||
|
||||
app.get('/repair-batches/:batchId/tickets', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
|
||||
const { batchId } = request.params as { batchId: string }
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await RepairTicketService.listByBatch(app.db, request.companyId, batchId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
// --- Repair Service Templates ---
|
||||
|
||||
app.post('/repair-service-templates', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||
const parsed = RepairServiceTemplateCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const template = await RepairServiceTemplateService.create(app.db, request.companyId, parsed.data)
|
||||
return reply.status(201).send(template)
|
||||
})
|
||||
|
||||
app.get('/repair-service-templates', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await RepairServiceTemplateService.list(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.patch('/repair-service-templates/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = RepairServiceTemplateUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const template = await RepairServiceTemplateService.update(app.db, request.companyId, id, parsed.data)
|
||||
if (!template) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
|
||||
return reply.send(template)
|
||||
})
|
||||
|
||||
app.delete('/repair-service-templates/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const template = await RepairServiceTemplateService.delete(app.db, request.companyId, id)
|
||||
if (!template) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
|
||||
return reply.send(template)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user