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:
Ryan Moon
2026-03-29 09:12:40 -05:00
parent 1d48f0befa
commit f17bbff02c
20 changed files with 2791 additions and 1 deletions

View File

@@ -0,0 +1,169 @@
import {
pgTable,
uuid,
varchar,
text,
timestamp,
boolean,
integer,
numeric,
pgEnum,
} from 'drizzle-orm/pg-core'
import { companies, locations } from './stores.js'
import { accounts } from './accounts.js'
import { inventoryUnits, products } from './inventory.js'
import { users } from './users.js'
// --- Enums ---
export const repairTicketStatusEnum = pgEnum('repair_ticket_status', [
'intake',
'diagnosing',
'pending_approval',
'approved',
'in_progress',
'pending_parts',
'ready',
'picked_up',
'delivered',
'cancelled',
])
export const repairLineItemTypeEnum = pgEnum('repair_line_item_type', [
'labor',
'part',
'flat_rate',
'misc',
])
export const repairConditionInEnum = pgEnum('repair_condition_in', [
'excellent',
'good',
'fair',
'poor',
])
export const repairBatchStatusEnum = pgEnum('repair_batch_status', [
'intake',
'in_progress',
'pending_approval',
'approved',
'completed',
'delivered',
'cancelled',
])
export const repairBatchApprovalEnum = pgEnum('repair_batch_approval', [
'pending',
'approved',
'rejected',
])
// --- Tables ---
// Defined before repairTickets because tickets FK to batches
export const repairBatches = pgTable('repair_batch', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
locationId: uuid('location_id').references(() => locations.id),
batchNumber: varchar('batch_number', { length: 50 }),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id),
contactName: varchar('contact_name', { length: 255 }),
contactPhone: varchar('contact_phone', { length: 50 }),
contactEmail: varchar('contact_email', { length: 255 }),
status: repairBatchStatusEnum('status').notNull().default('intake'),
approvalStatus: repairBatchApprovalEnum('approval_status').notNull().default('pending'),
approvedBy: uuid('approved_by').references(() => users.id),
approvedAt: timestamp('approved_at', { withTimezone: true }),
pickupDate: timestamp('pickup_date', { withTimezone: true }),
dueDate: timestamp('due_date', { withTimezone: true }),
completedDate: timestamp('completed_date', { withTimezone: true }),
deliveredDate: timestamp('delivered_date', { withTimezone: true }),
instrumentCount: integer('instrument_count').notNull().default(0),
receivedCount: integer('received_count').notNull().default(0),
estimatedTotal: numeric('estimated_total', { precision: 10, scale: 2 }),
actualTotal: numeric('actual_total', { precision: 10, scale: 2 }),
notes: text('notes'),
legacyId: varchar('legacy_id', { length: 255 }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
export const repairTickets = pgTable('repair_ticket', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
locationId: uuid('location_id').references(() => locations.id),
repairBatchId: uuid('repair_batch_id').references(() => repairBatches.id),
ticketNumber: varchar('ticket_number', { length: 50 }),
accountId: uuid('account_id').references(() => accounts.id),
customerName: varchar('customer_name', { length: 255 }).notNull(),
customerPhone: varchar('customer_phone', { length: 50 }),
inventoryUnitId: uuid('inventory_unit_id').references(() => inventoryUnits.id),
instrumentDescription: text('instrument_description'),
serialNumber: varchar('serial_number', { length: 255 }),
conditionIn: repairConditionInEnum('condition_in'),
conditionInNotes: text('condition_in_notes'),
problemDescription: text('problem_description').notNull(),
technicianNotes: text('technician_notes'),
status: repairTicketStatusEnum('status').notNull().default('intake'),
assignedTechnicianId: uuid('assigned_technician_id').references(() => users.id),
estimatedCost: numeric('estimated_cost', { precision: 10, scale: 2 }),
actualCost: numeric('actual_cost', { precision: 10, scale: 2 }),
intakeDate: timestamp('intake_date', { withTimezone: true }).notNull().defaultNow(),
promisedDate: timestamp('promised_date', { withTimezone: true }),
completedDate: timestamp('completed_date', { withTimezone: true }),
legacyId: varchar('legacy_id', { length: 255 }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
export const repairLineItems = pgTable('repair_line_item', {
id: uuid('id').primaryKey().defaultRandom(),
repairTicketId: uuid('repair_ticket_id')
.notNull()
.references(() => repairTickets.id),
itemType: repairLineItemTypeEnum('item_type').notNull(),
description: varchar('description', { length: 255 }).notNull(),
productId: uuid('product_id').references(() => products.id),
qty: numeric('qty', { precision: 10, scale: 3 }).notNull().default('1'),
unitPrice: numeric('unit_price', { precision: 10, scale: 2 }).notNull().default('0'),
totalPrice: numeric('total_price', { precision: 10, scale: 2 }).notNull().default('0'),
cost: numeric('cost', { precision: 10, scale: 2 }),
technicianId: uuid('technician_id').references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export const repairServiceTemplates = pgTable('repair_service_template', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
name: varchar('name', { length: 255 }).notNull(),
instrumentType: varchar('instrument_type', { length: 100 }),
size: varchar('size', { length: 50 }),
description: text('description'),
itemType: repairLineItemTypeEnum('item_type').notNull().default('flat_rate'),
defaultPrice: numeric('default_price', { precision: 10, scale: 2 }).notNull().default('0'),
defaultCost: numeric('default_cost', { precision: 10, scale: 2 }),
sortOrder: integer('sort_order').notNull().default(0),
isActive: boolean('is_active').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
// --- Type exports ---
export type RepairTicket = typeof repairTickets.$inferSelect
export type RepairTicketInsert = typeof repairTickets.$inferInsert
export type RepairLineItem = typeof repairLineItems.$inferSelect
export type RepairLineItemInsert = typeof repairLineItems.$inferInsert
export type RepairBatch = typeof repairBatches.$inferSelect
export type RepairBatchInsert = typeof repairBatches.$inferInsert
export type RepairServiceTemplate = typeof repairServiceTemplates.$inferSelect
export type RepairServiceTemplateInsert = typeof repairServiceTemplates.$inferInsert