feat: rework EOL, repairs, and hosts for real workflow
CI / Lint · Typecheck · Test · Build (push) Successful in 48s
CI / Playwright (smoke) (push) Has been skipped
CI / Build & push images (push) Successful in 1m1s

Four domain-model changes driven by exercising the deployed 2.0 build:

- EOL moves from manufacturer to MPN via new PartModel catalog table,
  so alerts fire on the thing that actually ages.
- Repairs re-home to Host (required hostId + problem text) with an
  optional RepairJobPart join for affected parts; drop Part.replacementPartId.
- New /repairs/:id detail page with editable problem, part list, and
  a RepairComment thread (REPAIR_COMMENTED events fan out to each
  problem part's timeline).
- Host.assetId (required, unique) surfaces prominently on the repair
  page so techs can confirm they're touching the right box.

Single destructive migration reshapes existing dev data. All 7 packages
typecheck clean; 30 API tests pass (9 new covering host membership,
upsertByMpn idempotency + race, assetId 409, comment userId stamping).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 10:17:29 -04:00
parent 23bd0f0c6a
commit 0f952d6c1b
50 changed files with 2665 additions and 602 deletions
+91 -49
View File
@@ -16,16 +16,17 @@ datasource db {
}
model User {
id String @id @default(uuid())
username String @unique
email String @unique
id String @id @default(uuid())
username String @unique
email String @unique
passwordHash String
role String @default("TECHNICIAN")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
role String @default("TECHNICIAN")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
partEvents PartEvent[]
refreshTokens RefreshToken[]
repairAssignments RepairJob[] @relation("RepairAssignee")
repairAssignments RepairJob[] @relation("RepairAssignee")
repairComments RepairComment[]
savedViews SavedView[]
csvImportJobs CsvImportJob[]
}
@@ -45,12 +46,28 @@ model RefreshToken {
}
model Manufacturer {
id String @id @default(uuid())
name String @unique
eolDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
id String @id @default(uuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
partModels PartModel[]
}
model PartModel {
id String @id @default(uuid())
manufacturerId String
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id], onDelete: Restrict)
mpn String
eolDate DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
@@unique([manufacturerId, mpn])
@@index([manufacturerId])
@@index([eolDate])
}
model Site {
@@ -97,33 +114,33 @@ model Category {
}
model Part {
id String @id @default(uuid())
serialNumber String @unique
mpn String
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)
replacementPartId String?
replacement Part? @relation("PartReplacement", fields: [replacementPartId], references: [id], onDelete: SetNull)
replacedBy Part[] @relation("PartReplacement")
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
events PartEvent[]
tags PartTag[]
repairs RepairJob[]
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[]
@@index([state])
@@index([binId])
@@index([manufacturerId])
@@index([mpn])
@@index([partModelId])
@@index([categoryId])
@@index([replacementPartId])
@@index([hostId])
}
model PartEvent {
@@ -164,36 +181,61 @@ model PartTag {
model Host {
id String @id @default(uuid())
assetId String @unique
name String @unique
location String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
repairs RepairJob[]
}
model RepairJob {
id String @id @default(uuid())
partId String
part Part @relation(fields: [partId], references: [id], onDelete: Cascade)
hostId String?
host Host? @relation(fields: [hostId], references: [id], onDelete: SetNull)
assigneeId String?
assignee User? @relation("RepairAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
status String @default("PENDING")
openedAt DateTime @default(now())
closedAt DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
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")
problem String
openedAt DateTime @default(now())
closedAt DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
problemParts RepairJobPart[]
comments RepairComment[]
@@index([partId])
@@index([status])
@@index([hostId])
@@index([assigneeId])
@@index([status, openedAt(sort: Desc)])
}
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())
@@id([repairJobId, partId])
@@index([partId])
}
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())
@@index([repairJobId, createdAt])
}
model WebhookSubscription {
id String @id @default(uuid())
url String