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:
78
packages/backend/src/db/migrations/0015_repairs.sql
Normal file
78
packages/backend/src/db/migrations/0015_repairs.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- Repair domain enums
|
||||
CREATE TYPE "repair_ticket_status" AS ENUM ('intake', 'diagnosing', 'pending_approval', 'approved', 'in_progress', 'pending_parts', 'ready', 'picked_up', 'delivered', 'cancelled');
|
||||
CREATE TYPE "repair_line_item_type" AS ENUM ('labor', 'part', 'flat_rate', 'misc');
|
||||
CREATE TYPE "repair_condition_in" AS ENUM ('excellent', 'good', 'fair', 'poor');
|
||||
CREATE TYPE "repair_batch_status" AS ENUM ('intake', 'in_progress', 'pending_approval', 'approved', 'completed', 'delivered', 'cancelled');
|
||||
CREATE TYPE "repair_batch_approval" AS ENUM ('pending', 'approved', 'rejected');
|
||||
|
||||
-- Repair batches (defined first — tickets FK to it)
|
||||
CREATE TABLE "repair_batch" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL REFERENCES "company"("id"),
|
||||
"location_id" uuid REFERENCES "location"("id"),
|
||||
"batch_number" varchar(50),
|
||||
"account_id" uuid NOT NULL REFERENCES "account"("id"),
|
||||
"contact_name" varchar(255),
|
||||
"contact_phone" varchar(50),
|
||||
"contact_email" varchar(255),
|
||||
"status" "repair_batch_status" NOT NULL DEFAULT 'intake',
|
||||
"approval_status" "repair_batch_approval" NOT NULL DEFAULT 'pending',
|
||||
"approved_by" uuid REFERENCES "user"("id"),
|
||||
"approved_at" timestamp with time zone,
|
||||
"pickup_date" timestamp with time zone,
|
||||
"due_date" timestamp with time zone,
|
||||
"completed_date" timestamp with time zone,
|
||||
"delivered_date" timestamp with time zone,
|
||||
"instrument_count" integer NOT NULL DEFAULT 0,
|
||||
"received_count" integer NOT NULL DEFAULT 0,
|
||||
"estimated_total" numeric(10, 2),
|
||||
"actual_total" numeric(10, 2),
|
||||
"notes" text,
|
||||
"legacy_id" varchar(255),
|
||||
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Repair tickets
|
||||
CREATE TABLE "repair_ticket" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL REFERENCES "company"("id"),
|
||||
"location_id" uuid REFERENCES "location"("id"),
|
||||
"repair_batch_id" uuid REFERENCES "repair_batch"("id"),
|
||||
"ticket_number" varchar(50),
|
||||
"account_id" uuid REFERENCES "account"("id"),
|
||||
"customer_name" varchar(255) NOT NULL,
|
||||
"customer_phone" varchar(50),
|
||||
"inventory_unit_id" uuid REFERENCES "inventory_unit"("id"),
|
||||
"instrument_description" text,
|
||||
"serial_number" varchar(255),
|
||||
"condition_in" "repair_condition_in",
|
||||
"condition_in_notes" text,
|
||||
"problem_description" text NOT NULL,
|
||||
"technician_notes" text,
|
||||
"status" "repair_ticket_status" NOT NULL DEFAULT 'intake',
|
||||
"assigned_technician_id" uuid REFERENCES "user"("id"),
|
||||
"estimated_cost" numeric(10, 2),
|
||||
"actual_cost" numeric(10, 2),
|
||||
"intake_date" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"promised_date" timestamp with time zone,
|
||||
"completed_date" timestamp with time zone,
|
||||
"legacy_id" varchar(255),
|
||||
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Repair line items
|
||||
CREATE TABLE "repair_line_item" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"repair_ticket_id" uuid NOT NULL REFERENCES "repair_ticket"("id"),
|
||||
"item_type" "repair_line_item_type" NOT NULL,
|
||||
"description" varchar(255) NOT NULL,
|
||||
"product_id" uuid REFERENCES "product"("id"),
|
||||
"qty" numeric(10, 3) NOT NULL DEFAULT 1,
|
||||
"unit_price" numeric(10, 2) NOT NULL DEFAULT 0,
|
||||
"total_price" numeric(10, 2) NOT NULL DEFAULT 0,
|
||||
"cost" numeric(10, 2),
|
||||
"technician_id" uuid REFERENCES "user"("id"),
|
||||
"created_at" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE "repair_service_template" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL REFERENCES "company"("id"),
|
||||
"name" varchar(255) NOT NULL,
|
||||
"instrument_type" varchar(100),
|
||||
"size" varchar(50),
|
||||
"description" text,
|
||||
"item_type" "repair_line_item_type" NOT NULL DEFAULT 'flat_rate',
|
||||
"default_price" numeric(10, 2) NOT NULL DEFAULT 0,
|
||||
"default_cost" numeric(10, 2),
|
||||
"sort_order" integer NOT NULL DEFAULT 0,
|
||||
"is_active" boolean NOT NULL DEFAULT true,
|
||||
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -106,6 +106,20 @@
|
||||
"when": 1774740000000,
|
||||
"tag": "0014_user_is_active",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "7",
|
||||
"when": 1774750000000,
|
||||
"tag": "0015_repairs",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "7",
|
||||
"when": 1774760000000,
|
||||
"tag": "0016_repair_service_templates",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
169
packages/backend/src/db/schema/repairs.ts
Normal file
169
packages/backend/src/db/schema/repairs.ts
Normal 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
|
||||
@@ -15,6 +15,7 @@ import { productRoutes } from './routes/v1/products.js'
|
||||
import { lookupRoutes } from './routes/v1/lookups.js'
|
||||
import { fileRoutes } from './routes/v1/files.js'
|
||||
import { rbacRoutes } from './routes/v1/rbac.js'
|
||||
import { repairRoutes } from './routes/v1/repairs.js'
|
||||
import { RbacService } from './services/rbac.service.js'
|
||||
|
||||
export async function buildApp() {
|
||||
@@ -65,6 +66,7 @@ export async function buildApp() {
|
||||
await app.register(lookupRoutes, { prefix: '/v1' })
|
||||
await app.register(fileRoutes, { prefix: '/v1' })
|
||||
await app.register(rbacRoutes, { prefix: '/v1' })
|
||||
await app.register(repairRoutes, { prefix: '/v1' })
|
||||
|
||||
// Auto-seed system permissions on startup
|
||||
app.addHook('onReady', async () => {
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
427
packages/backend/src/services/repair.service.ts
Normal file
427
packages/backend/src/services/repair.service.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
import { eq, and, count, type Column } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import {
|
||||
repairTickets,
|
||||
repairLineItems,
|
||||
repairBatches,
|
||||
repairServiceTemplates,
|
||||
} from '../db/schema/repairs.js'
|
||||
import type {
|
||||
RepairTicketCreateInput,
|
||||
RepairTicketUpdateInput,
|
||||
RepairLineItemCreateInput,
|
||||
RepairLineItemUpdateInput,
|
||||
RepairBatchCreateInput,
|
||||
RepairBatchUpdateInput,
|
||||
RepairServiceTemplateCreateInput,
|
||||
RepairServiceTemplateUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
buildSearchCondition,
|
||||
paginatedResponse,
|
||||
} from '../utils/pagination.js'
|
||||
|
||||
async function generateUniqueNumber(
|
||||
db: PostgresJsDatabase<any>,
|
||||
table: typeof repairTickets | typeof repairBatches,
|
||||
column: typeof repairTickets.ticketNumber | typeof repairBatches.batchNumber,
|
||||
companyId: string,
|
||||
companyIdColumn: Column,
|
||||
): Promise<string> {
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||||
const [existing] = await db
|
||||
.select({ id: table.id })
|
||||
.from(table)
|
||||
.where(and(eq(companyIdColumn, companyId), eq(column, num)))
|
||||
.limit(1)
|
||||
if (!existing) return num
|
||||
}
|
||||
return String(Math.floor(10000000 + Math.random() * 90000000))
|
||||
}
|
||||
|
||||
export const RepairTicketService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairTicketCreateInput) {
|
||||
const ticketNumber = await generateUniqueNumber(
|
||||
db, repairTickets, repairTickets.ticketNumber, companyId, repairTickets.companyId,
|
||||
)
|
||||
|
||||
const [ticket] = await db
|
||||
.insert(repairTickets)
|
||||
.values({
|
||||
companyId,
|
||||
ticketNumber,
|
||||
customerName: input.customerName,
|
||||
customerPhone: input.customerPhone,
|
||||
accountId: input.accountId,
|
||||
locationId: input.locationId,
|
||||
repairBatchId: input.repairBatchId,
|
||||
inventoryUnitId: input.inventoryUnitId,
|
||||
instrumentDescription: input.instrumentDescription,
|
||||
serialNumber: input.serialNumber,
|
||||
conditionIn: input.conditionIn,
|
||||
conditionInNotes: input.conditionInNotes,
|
||||
problemDescription: input.problemDescription,
|
||||
technicianNotes: input.technicianNotes,
|
||||
assignedTechnicianId: input.assignedTechnicianId,
|
||||
estimatedCost: input.estimatedCost?.toString(),
|
||||
promisedDate: input.promisedDate ? new Date(input.promisedDate) : undefined,
|
||||
})
|
||||
.returning()
|
||||
return ticket
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
const [ticket] = await db
|
||||
.select()
|
||||
.from(repairTickets)
|
||||
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.limit(1)
|
||||
return ticket ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(repairTickets.companyId, companyId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [
|
||||
repairTickets.ticketNumber,
|
||||
repairTickets.customerName,
|
||||
repairTickets.customerPhone,
|
||||
repairTickets.instrumentDescription,
|
||||
repairTickets.serialNumber,
|
||||
])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
ticket_number: repairTickets.ticketNumber,
|
||||
customer_name: repairTickets.customerName,
|
||||
status: repairTickets.status,
|
||||
intake_date: repairTickets.intakeDate,
|
||||
promised_date: repairTickets.promisedDate,
|
||||
created_at: repairTickets.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(repairTickets).where(where).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, repairTickets.createdAt)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(repairTickets).where(where),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async listByBatch(db: PostgresJsDatabase<any>, companyId: string, batchId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(repairTickets.companyId, companyId), eq(repairTickets.repairBatchId, batchId))
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.instrumentDescription])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
ticket_number: repairTickets.ticketNumber,
|
||||
customer_name: repairTickets.customerName,
|
||||
status: repairTickets.status,
|
||||
created_at: repairTickets.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(repairTickets).where(where).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, repairTickets.createdAt)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(repairTickets).where(where),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairTicketUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.estimatedCost !== undefined) values.estimatedCost = input.estimatedCost.toString()
|
||||
if (input.promisedDate !== undefined) values.promisedDate = input.promisedDate ? new Date(input.promisedDate) : null
|
||||
|
||||
const [ticket] = await db
|
||||
.update(repairTickets)
|
||||
.set(values)
|
||||
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.returning()
|
||||
return ticket ?? null
|
||||
},
|
||||
|
||||
async updateStatus(db: PostgresJsDatabase<any>, companyId: string, id: string, status: string) {
|
||||
const updates: Record<string, unknown> = { status, updatedAt: new Date() }
|
||||
if (status === 'ready' || status === 'picked_up' || status === 'delivered') {
|
||||
updates.completedDate = new Date()
|
||||
}
|
||||
|
||||
const [ticket] = await db
|
||||
.update(repairTickets)
|
||||
.set(updates)
|
||||
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.returning()
|
||||
return ticket ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
// Soft-cancel: set status to cancelled rather than hard delete
|
||||
const [ticket] = await db
|
||||
.update(repairTickets)
|
||||
.set({ status: 'cancelled', updatedAt: new Date() })
|
||||
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.returning()
|
||||
return ticket ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const RepairLineItemService = {
|
||||
async create(db: PostgresJsDatabase<any>, input: RepairLineItemCreateInput) {
|
||||
const [item] = await db
|
||||
.insert(repairLineItems)
|
||||
.values({
|
||||
repairTicketId: input.repairTicketId,
|
||||
itemType: input.itemType,
|
||||
description: input.description,
|
||||
productId: input.productId,
|
||||
qty: input.qty?.toString(),
|
||||
unitPrice: input.unitPrice?.toString(),
|
||||
totalPrice: input.totalPrice?.toString(),
|
||||
cost: input.cost?.toString(),
|
||||
technicianId: input.technicianId,
|
||||
})
|
||||
.returning()
|
||||
return item
|
||||
},
|
||||
|
||||
async listByTicket(db: PostgresJsDatabase<any>, ticketId: string, params: PaginationInput) {
|
||||
const where = eq(repairLineItems.repairTicketId, ticketId)
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
item_type: repairLineItems.itemType,
|
||||
created_at: repairLineItems.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(repairLineItems).where(where).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, repairLineItems.createdAt)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(repairLineItems).where(where),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: RepairLineItemUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input }
|
||||
if (input.qty !== undefined) values.qty = input.qty.toString()
|
||||
if (input.unitPrice !== undefined) values.unitPrice = input.unitPrice.toString()
|
||||
if (input.totalPrice !== undefined) values.totalPrice = input.totalPrice.toString()
|
||||
if (input.cost !== undefined) values.cost = input.cost.toString()
|
||||
|
||||
const [item] = await db
|
||||
.update(repairLineItems)
|
||||
.set(values)
|
||||
.where(eq(repairLineItems.id, id))
|
||||
.returning()
|
||||
return item ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [item] = await db
|
||||
.delete(repairLineItems)
|
||||
.where(eq(repairLineItems.id, id))
|
||||
.returning()
|
||||
return item ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const RepairBatchService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairBatchCreateInput) {
|
||||
const batchNumber = await generateUniqueNumber(
|
||||
db, repairBatches, repairBatches.batchNumber, companyId, repairBatches.companyId,
|
||||
)
|
||||
|
||||
const [batch] = await db
|
||||
.insert(repairBatches)
|
||||
.values({
|
||||
companyId,
|
||||
batchNumber,
|
||||
accountId: input.accountId,
|
||||
locationId: input.locationId,
|
||||
contactName: input.contactName,
|
||||
contactPhone: input.contactPhone,
|
||||
contactEmail: input.contactEmail,
|
||||
pickupDate: input.pickupDate ? new Date(input.pickupDate) : undefined,
|
||||
dueDate: input.dueDate ? new Date(input.dueDate) : undefined,
|
||||
instrumentCount: input.instrumentCount,
|
||||
notes: input.notes,
|
||||
})
|
||||
.returning()
|
||||
return batch
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
const [batch] = await db
|
||||
.select()
|
||||
.from(repairBatches)
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.limit(1)
|
||||
return batch ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(repairBatches.companyId, companyId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairBatches.batchNumber, repairBatches.contactName, repairBatches.contactEmail])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
batch_number: repairBatches.batchNumber,
|
||||
status: repairBatches.status,
|
||||
due_date: repairBatches.dueDate,
|
||||
created_at: repairBatches.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(repairBatches).where(where).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, repairBatches.createdAt)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(repairBatches).where(where),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairBatchUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.pickupDate !== undefined) values.pickupDate = input.pickupDate ? new Date(input.pickupDate) : null
|
||||
if (input.dueDate !== undefined) values.dueDate = input.dueDate ? new Date(input.dueDate) : null
|
||||
|
||||
const [batch] = await db
|
||||
.update(repairBatches)
|
||||
.set(values)
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.returning()
|
||||
return batch ?? null
|
||||
},
|
||||
|
||||
async updateStatus(db: PostgresJsDatabase<any>, companyId: string, id: string, status: string) {
|
||||
const updates: Record<string, unknown> = { status, updatedAt: new Date() }
|
||||
if (status === 'completed') updates.completedDate = new Date()
|
||||
if (status === 'delivered') updates.deliveredDate = new Date()
|
||||
|
||||
const [batch] = await db
|
||||
.update(repairBatches)
|
||||
.set(updates)
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.returning()
|
||||
return batch ?? null
|
||||
},
|
||||
|
||||
async approve(db: PostgresJsDatabase<any>, companyId: string, id: string, approvedBy: string) {
|
||||
const [batch] = await db
|
||||
.update(repairBatches)
|
||||
.set({
|
||||
approvalStatus: 'approved',
|
||||
approvedBy,
|
||||
approvedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.returning()
|
||||
return batch ?? null
|
||||
},
|
||||
|
||||
async reject(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
const [batch] = await db
|
||||
.update(repairBatches)
|
||||
.set({
|
||||
approvalStatus: 'rejected',
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.returning()
|
||||
return batch ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const RepairServiceTemplateService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairServiceTemplateCreateInput) {
|
||||
const [template] = await db
|
||||
.insert(repairServiceTemplates)
|
||||
.values({
|
||||
companyId,
|
||||
name: input.name,
|
||||
instrumentType: input.instrumentType,
|
||||
size: input.size,
|
||||
description: input.description,
|
||||
itemType: input.itemType,
|
||||
defaultPrice: input.defaultPrice?.toString(),
|
||||
defaultCost: input.defaultCost?.toString(),
|
||||
sortOrder: input.sortOrder,
|
||||
})
|
||||
.returning()
|
||||
return template
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(repairServiceTemplates.companyId, companyId), eq(repairServiceTemplates.isActive, true))
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.instrumentType, repairServiceTemplates.size, repairServiceTemplates.description])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
name: repairServiceTemplates.name,
|
||||
instrument_type: repairServiceTemplates.instrumentType,
|
||||
default_price: repairServiceTemplates.defaultPrice,
|
||||
sort_order: repairServiceTemplates.sortOrder,
|
||||
created_at: repairServiceTemplates.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(repairServiceTemplates).where(where).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, repairServiceTemplates.sortOrder)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(repairServiceTemplates).where(where),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairServiceTemplateUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.defaultPrice !== undefined) values.defaultPrice = input.defaultPrice.toString()
|
||||
if (input.defaultCost !== undefined) values.defaultCost = input.defaultCost.toString()
|
||||
|
||||
const [template] = await db
|
||||
.update(repairServiceTemplates)
|
||||
.set(values)
|
||||
.where(and(eq(repairServiceTemplates.id, id), eq(repairServiceTemplates.companyId, companyId)))
|
||||
.returning()
|
||||
return template ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
const [template] = await db
|
||||
.update(repairServiceTemplates)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(and(eq(repairServiceTemplates.id, id), eq(repairServiceTemplates.companyId, companyId)))
|
||||
.returning()
|
||||
return template ?? null
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user