v0.13.0 — Inventory (HomeBox): durable items that nest
The first inventory vertical on the shared spine. Things you own — tools, gear, equipment — each living in a Location or inside another Item. - Item model (kind DURABLE|CONSUMABLE; consumable fields present but unused so Pantry needs no migration), self-nesting via parentItemId (toolbox holds drill, with a cycle guard), value/warranty/serial/brand/qty, qrSlug, photo - Label (m2m) + ItemAttachment (photos/manuals/receipts) + ItemLog (maintenance/repair/move/dispose) history; Task.itemId - API-first: /api/v1/items (CRUD), labels, logs, multipart attachments — the UI consumes the same scoped endpoints. items service shared with the coming MCP - Inventory pages: tree grouped by Location with items nested inside items; item detail with warranty status, photos, contents, history; add/edit/delete, log, photo upload. Nav entry. Backup/restore cover the new tables - Pure value.ts (warrantyStatus, totalValue) with tests; verified end-to-end against a running server (nest, cycle-guard 400, log 201) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -100,6 +100,7 @@ model Location {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
plants Plant[]
|
||||
items Item[]
|
||||
|
||||
@@index([parentId])
|
||||
@@index([kind])
|
||||
@@ -236,6 +237,8 @@ model Task {
|
||||
category TaskCategory @default(GARDEN)
|
||||
plantId String?
|
||||
plant Plant? @relation(fields: [plantId], references: [id])
|
||||
itemId String?
|
||||
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull)
|
||||
recurrence RecurrenceType @default(YEARLY)
|
||||
intervalDays Int? // for CUSTOM: every N days
|
||||
month Int? // 1–12: for YEARLY, which month
|
||||
@@ -248,6 +251,7 @@ model Task {
|
||||
|
||||
@@index([nextDue])
|
||||
@@index([plantId])
|
||||
@@index([itemId])
|
||||
@@index([active])
|
||||
}
|
||||
|
||||
@@ -276,3 +280,118 @@ model PlantLog {
|
||||
@@index([plantId])
|
||||
@@index([date])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inventory — things you own. Durable items (tools, gear) and, later,
|
||||
// consumables (food, supplies) share this one table, told apart by `kind`.
|
||||
// Items nest: an Item lives in a Location OR inside another Item (toolbox →
|
||||
// drill). Consumable-only fields (barcode, bestBefore, minStock) are unused
|
||||
// by the durable UI but present so Pantry needs no schema change.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum ItemKind {
|
||||
DURABLE // tools, gear, equipment — "where is it?"
|
||||
CONSUMABLE // food, feed, supplies — "do I have it?" (Pantry phase)
|
||||
}
|
||||
|
||||
model Item {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
kind ItemKind @default(DURABLE)
|
||||
quantity Decimal @default(1) @db.Decimal(10, 2)
|
||||
unit String? // "each", "lbs", "bottles"
|
||||
|
||||
// Where it lives — a place, or inside another item (containers are items too).
|
||||
locationId String?
|
||||
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
|
||||
parentItemId String?
|
||||
parentItem Item? @relation("ItemNesting", fields: [parentItemId], references: [id], onDelete: SetNull)
|
||||
containedItems Item[] @relation("ItemNesting")
|
||||
|
||||
// Durable attributes
|
||||
value Decimal? @db.Decimal(10, 2)
|
||||
purchaseDate DateTime?
|
||||
warrantyExpiry DateTime?
|
||||
serial String?
|
||||
modelNumber String?
|
||||
brand String?
|
||||
|
||||
// Consumable attributes (Pantry phase)
|
||||
barcode String?
|
||||
bestBefore DateTime?
|
||||
minStock Decimal? @db.Decimal(10, 2)
|
||||
|
||||
// Shared
|
||||
qrSlug String? @unique // short slug for a printed QR label
|
||||
imageUrl String?
|
||||
notes String?
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
labels LabelOnItem[]
|
||||
attachments ItemAttachment[]
|
||||
logs ItemLog[]
|
||||
tasks Task[]
|
||||
|
||||
@@index([locationId])
|
||||
@@index([parentItemId])
|
||||
@@index([kind])
|
||||
@@index([active])
|
||||
@@index([name])
|
||||
@@index([barcode])
|
||||
}
|
||||
|
||||
model Label {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
color String?
|
||||
items LabelOnItem[]
|
||||
}
|
||||
|
||||
model LabelOnItem {
|
||||
itemId String
|
||||
labelId String
|
||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
label Label @relation(fields: [labelId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([itemId, labelId])
|
||||
@@index([labelId])
|
||||
}
|
||||
|
||||
model ItemAttachment {
|
||||
id String @id @default(cuid())
|
||||
itemId String
|
||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
kind String // "PHOTO" | "MANUAL" | "RECEIPT"
|
||||
url String
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([itemId])
|
||||
}
|
||||
|
||||
enum ItemLogType {
|
||||
NOTE
|
||||
MAINTENANCE
|
||||
MOVED
|
||||
PURCHASED
|
||||
REPAIRED
|
||||
DISPOSED
|
||||
}
|
||||
|
||||
model ItemLog {
|
||||
id String @id @default(cuid())
|
||||
itemId String
|
||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
type ItemLogType
|
||||
date DateTime @default(now())
|
||||
notes String?
|
||||
cost Decimal? @db.Decimal(10, 2)
|
||||
actorId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([itemId])
|
||||
@@index([date])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user