feat: split Repairs into FM, Repair, and Custody workflows
The old Repairs module had grown ticketing-system features (status lifecycle, comments, assignee, notes) that duplicate what the external ticketing tool already owns. Vector only needs to track whether maintenance is open or closed. - Rename RepairJob -> Fm (OPEN/CLOSED only), drop RepairComment, assignee, notes - New Repair table: persistent log of physical part swaps, with ingest on unknown broken MPN via partModels.upsertByMpn - New custody model: PENDING_DROP_IN_CUSTODY / PENDING_DESTRUCTION_IN_CUSTODY states + Part.custodianId, with a "My Custody" page for drop-off - PartModel.destroyOnFail routes broken parts to the destruction path - Host lookup on /fms and /repairs accepts hostId XOR assetId - Wire the dormant webhook emitter: fm.opened, fm.closed, repair.logged - Single fresh Prisma migration (dev DB was wiped, no backfill) Tests: 60 passing (custody transitions in parts.test.ts; new fms.test.ts, repairs.test.ts, custody.test.ts covering happy paths, validation failures, webhook emissions, and ingest-on-unknown-MPN). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -16,19 +16,19 @@ datasource db {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
email String @unique
|
||||
passwordHash String
|
||||
role String @default("TECHNICIAN")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
partEvents PartEvent[]
|
||||
refreshTokens RefreshToken[]
|
||||
repairAssignments RepairJob[] @relation("RepairAssignee")
|
||||
repairComments RepairComment[]
|
||||
savedViews SavedView[]
|
||||
csvImportJobs CsvImportJob[]
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
email String @unique
|
||||
passwordHash String
|
||||
role String @default("TECHNICIAN")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
partEvents PartEvent[]
|
||||
refreshTokens RefreshToken[]
|
||||
custodyParts Part[] @relation("Custody")
|
||||
repairs Repair[]
|
||||
savedViews SavedView[]
|
||||
csvImportJobs CsvImportJob[]
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
@@ -60,6 +60,7 @@ model PartModel {
|
||||
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id], onDelete: Restrict)
|
||||
mpn String
|
||||
eolDate DateTime?
|
||||
destroyOnFail Boolean @default(false)
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -114,26 +115,30 @@ model Category {
|
||||
}
|
||||
|
||||
model Part {
|
||||
id String @id @default(uuid())
|
||||
serialNumber String @unique
|
||||
partModelId String
|
||||
partModel PartModel @relation(fields: [partModelId], references: [id], onDelete: Restrict)
|
||||
manufacturerId String
|
||||
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id], onDelete: Restrict)
|
||||
price Float?
|
||||
state String @default("SPARE")
|
||||
binId String?
|
||||
bin Bin? @relation(fields: [binId], references: [id], onDelete: SetNull)
|
||||
categoryId String?
|
||||
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
||||
hostId String?
|
||||
host Host? @relation(fields: [hostId], references: [id], onDelete: SetNull)
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
events PartEvent[]
|
||||
tags PartTag[]
|
||||
problemInRepairs RepairJobPart[]
|
||||
id String @id @default(uuid())
|
||||
serialNumber String @unique
|
||||
partModelId String
|
||||
partModel PartModel @relation(fields: [partModelId], references: [id], onDelete: Restrict)
|
||||
manufacturerId String
|
||||
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id], onDelete: Restrict)
|
||||
price Float?
|
||||
state String @default("SPARE")
|
||||
binId String?
|
||||
bin Bin? @relation(fields: [binId], references: [id], onDelete: SetNull)
|
||||
categoryId String?
|
||||
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
||||
hostId String?
|
||||
host Host? @relation(fields: [hostId], references: [id], onDelete: SetNull)
|
||||
custodianId String?
|
||||
custodian User? @relation("Custody", fields: [custodianId], references: [id], onDelete: SetNull)
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
events PartEvent[]
|
||||
tags PartTag[]
|
||||
problemInFms FmPart[]
|
||||
brokenRepairs Repair[] @relation("BrokenRepairs")
|
||||
replacementRepairs Repair[] @relation("ReplacementRepairs")
|
||||
|
||||
@@index([state])
|
||||
@@index([binId])
|
||||
@@ -141,6 +146,7 @@ model Part {
|
||||
@@index([partModelId])
|
||||
@@index([categoryId])
|
||||
@@index([hostId])
|
||||
@@index([custodianId])
|
||||
}
|
||||
|
||||
model PartEvent {
|
||||
@@ -180,60 +186,71 @@ model PartTag {
|
||||
}
|
||||
|
||||
model Host {
|
||||
id String @id @default(uuid())
|
||||
assetId String @unique
|
||||
name String @unique
|
||||
id String @id @default(uuid())
|
||||
assetId String @unique
|
||||
name String @unique
|
||||
location String?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
parts Part[]
|
||||
repairs RepairJob[]
|
||||
fms Fm[]
|
||||
repairs Repair[]
|
||||
}
|
||||
|
||||
model RepairJob {
|
||||
id String @id @default(uuid())
|
||||
model Fm {
|
||||
id String @id @default(uuid())
|
||||
hostId String
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Restrict)
|
||||
assigneeId String?
|
||||
assignee User? @relation("RepairAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
|
||||
status String @default("PENDING")
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Restrict)
|
||||
status String @default("OPEN")
|
||||
problem String
|
||||
openedAt DateTime @default(now())
|
||||
openedAt DateTime @default(now())
|
||||
closedAt DateTime?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
problemParts RepairJobPart[]
|
||||
comments RepairComment[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
problemParts FmPart[]
|
||||
repairs Repair[]
|
||||
|
||||
@@index([status])
|
||||
@@index([hostId])
|
||||
@@index([assigneeId])
|
||||
@@index([status, openedAt(sort: Desc)])
|
||||
@@map("fms")
|
||||
}
|
||||
|
||||
model RepairJobPart {
|
||||
repairJobId String
|
||||
partId String
|
||||
repairJob RepairJob @relation(fields: [repairJobId], references: [id], onDelete: Cascade)
|
||||
part Part @relation(fields: [partId], references: [id], onDelete: Restrict)
|
||||
createdAt DateTime @default(now())
|
||||
model FmPart {
|
||||
fmId String
|
||||
partId String
|
||||
fm Fm @relation(fields: [fmId], references: [id], onDelete: Cascade)
|
||||
part Part @relation(fields: [partId], references: [id], onDelete: Restrict)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@id([repairJobId, partId])
|
||||
@@id([fmId, partId])
|
||||
@@index([partId])
|
||||
@@map("fm_parts")
|
||||
}
|
||||
|
||||
model RepairComment {
|
||||
id String @id @default(uuid())
|
||||
repairJobId String
|
||||
repairJob RepairJob @relation(fields: [repairJobId], references: [id], onDelete: Cascade)
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
content String
|
||||
createdAt DateTime @default(now())
|
||||
model Repair {
|
||||
id String @id @default(uuid())
|
||||
hostId String
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Restrict)
|
||||
brokenPartId String
|
||||
brokenPart Part @relation("BrokenRepairs", fields: [brokenPartId], references: [id], onDelete: Restrict)
|
||||
replacementPartId String
|
||||
replacement Part @relation("ReplacementRepairs", fields: [replacementPartId], references: [id], onDelete: Restrict)
|
||||
performedById String
|
||||
performedBy User @relation(fields: [performedById], references: [id], onDelete: Restrict)
|
||||
performedAt DateTime @default(now())
|
||||
fmId String?
|
||||
fm Fm? @relation(fields: [fmId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([repairJobId, createdAt])
|
||||
@@index([hostId, performedAt(sort: Desc)])
|
||||
@@index([fmId])
|
||||
@@index([performedById, performedAt(sort: Desc)])
|
||||
@@index([brokenPartId])
|
||||
@@index([replacementPartId])
|
||||
@@map("repairs")
|
||||
}
|
||||
|
||||
model WebhookSubscription {
|
||||
|
||||
Reference in New Issue
Block a user