v0.15.0 — Inventory depth (HomeBox parity) + Locations page

Rounds out Inventory toward HomeBox, and gives Locations a real home.

Inventory:
- Attach manuals, receipts, and warranty docs (any file), not just photos —
  attachment uploader now has a kind picker; WARRANTY kind added
- Richer purchase/warranty: purchasedFrom, warrantyDetails, lifetimeWarranty,
  insured (item dialog + detail facts + header badge)
- Schedule recurring maintenance from an item (creates a Task linked via
  itemId); item detail shows its maintenance with overdue flagged. Task create
  now accepts itemId/animalId

Locations:
- New Locations page + nav: browse/add/rename/move/delete the whole place tree
  (LocationManager over the existing /api/locations routes); shows plant/item
  counts per place. The spot to merge near-dupes before dropping legacy zone

Verified end-to-end against a running server (new fields round-trip, maintenance
task links to item, MANUAL attachment stored). tsc + 33 tests + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 00:34:07 -05:00
parent cffa739056
commit 63569aabd3
15 changed files with 481 additions and 21 deletions

View File

@@ -33,7 +33,11 @@ export type ItemFields = {
serial: string;
value: string;
purchaseDate: string;
purchaseFrom: string;
warrantyExpiry: string;
warrantyDetails: string;
lifetimeWarranty: boolean;
insured: boolean;
barcode: string;
notes: string;
labelIds: string[];
@@ -42,7 +46,8 @@ export type ItemFields = {
export function blankItem(): ItemFields {
return {
name: "", description: "", quantity: "1", unit: "", locationId: "", parentItemId: "",
brand: "", modelNumber: "", serial: "", value: "", purchaseDate: "", warrantyExpiry: "",
brand: "", modelNumber: "", serial: "", value: "", purchaseDate: "", purchaseFrom: "",
warrantyExpiry: "", warrantyDetails: "", lifetimeWarranty: false, insured: false,
barcode: "", notes: "", labelIds: [],
};
}
@@ -109,7 +114,11 @@ function ItemDialog({
serial: f.serial.trim() || undefined,
value: f.value ? Number(f.value) : null,
purchaseDate: f.purchaseDate || null,
purchaseFrom: f.purchaseFrom.trim() || undefined,
warrantyExpiry: f.warrantyExpiry || null,
warrantyDetails: f.warrantyDetails.trim() || undefined,
lifetimeWarranty: f.lifetimeWarranty,
insured: f.insured,
barcode: f.barcode.trim() || undefined,
notes: f.notes.trim() || undefined,
labelIds: f.labelIds,
@@ -197,7 +206,26 @@ function ItemDialog({
</div>
<div className="space-y-1.5">
<Label>Warranty until</Label>
<Input type="date" value={f.warrantyExpiry} onChange={(e) => set("warrantyExpiry", e.target.value)} />
<Input type="date" value={f.warrantyExpiry} onChange={(e) => set("warrantyExpiry", e.target.value)} disabled={f.lifetimeWarranty} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Purchased from</Label>
<Input value={f.purchaseFrom} onChange={(e) => set("purchaseFrom", e.target.value)} placeholder="Home Depot, Amazon, gift…" />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Warranty details</Label>
<Textarea rows={2} value={f.warrantyDetails} onChange={(e) => set("warrantyDetails", e.target.value)} placeholder="Coverage, claim info…" />
</div>
<div className="col-span-2 flex flex-wrap gap-x-6 gap-y-2">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" checked={f.lifetimeWarranty} onChange={(e) => set("lifetimeWarranty", e.target.checked)} className="accent-[hsl(var(--leaf))]" />
Lifetime warranty
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" checked={f.insured} onChange={(e) => set("insured", e.target.checked)} className="accent-[hsl(var(--leaf))]" />
Insured
</label>
</div>
<div className="col-span-2 space-y-1.5">