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:
143
prisma/migrations/20260616050040_v0_13_inventory/migration.sql
Normal file
143
prisma/migrations/20260616050040_v0_13_inventory/migration.sql
Normal file
@@ -0,0 +1,143 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ItemKind" AS ENUM ('DURABLE', 'CONSUMABLE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ItemLogType" AS ENUM ('NOTE', 'MAINTENANCE', 'MOVED', 'PURCHASED', 'REPAIRED', 'DISPOSED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "PlantGroup" ALTER COLUMN "updatedAt" DROP DEFAULT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Task" ADD COLUMN "itemId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Item" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"kind" "ItemKind" NOT NULL DEFAULT 'DURABLE',
|
||||
"quantity" DECIMAL(10,2) NOT NULL DEFAULT 1,
|
||||
"unit" TEXT,
|
||||
"locationId" TEXT,
|
||||
"parentItemId" TEXT,
|
||||
"value" DECIMAL(10,2),
|
||||
"purchaseDate" TIMESTAMP(3),
|
||||
"warrantyExpiry" TIMESTAMP(3),
|
||||
"serial" TEXT,
|
||||
"modelNumber" TEXT,
|
||||
"brand" TEXT,
|
||||
"barcode" TEXT,
|
||||
"bestBefore" TIMESTAMP(3),
|
||||
"minStock" DECIMAL(10,2),
|
||||
"qrSlug" TEXT,
|
||||
"imageUrl" TEXT,
|
||||
"notes" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Item_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Label" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"color" TEXT,
|
||||
|
||||
CONSTRAINT "Label_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LabelOnItem" (
|
||||
"itemId" TEXT NOT NULL,
|
||||
"labelId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "LabelOnItem_pkey" PRIMARY KEY ("itemId","labelId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ItemAttachment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"itemId" TEXT NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ItemAttachment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ItemLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"itemId" TEXT NOT NULL,
|
||||
"type" "ItemLogType" NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"notes" TEXT,
|
||||
"cost" DECIMAL(10,2),
|
||||
"actorId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ItemLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Item_qrSlug_key" ON "Item"("qrSlug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Item_locationId_idx" ON "Item"("locationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Item_parentItemId_idx" ON "Item"("parentItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Item_kind_idx" ON "Item"("kind");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Item_active_idx" ON "Item"("active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Item_name_idx" ON "Item"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Item_barcode_idx" ON "Item"("barcode");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Label_name_key" ON "Label"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LabelOnItem_labelId_idx" ON "LabelOnItem"("labelId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ItemAttachment_itemId_idx" ON "ItemAttachment"("itemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ItemLog_itemId_idx" ON "ItemLog"("itemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ItemLog_date_idx" ON "ItemLog"("date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Task_itemId_idx" ON "Task"("itemId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Task" ADD CONSTRAINT "Task_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Item" ADD CONSTRAINT "Item_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Item" ADD CONSTRAINT "Item_parentItemId_fkey" FOREIGN KEY ("parentItemId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LabelOnItem" ADD CONSTRAINT "LabelOnItem_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LabelOnItem" ADD CONSTRAINT "LabelOnItem_labelId_fkey" FOREIGN KEY ("labelId") REFERENCES "Label"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ItemAttachment" ADD CONSTRAINT "ItemAttachment_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ItemLog" ADD CONSTRAINT "ItemLog_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
provider = "postgresql"
|
||||
@@ -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