diff --git a/prisma/migrations/20260616052619_v0_15_item_details/migration.sql b/prisma/migrations/20260616052619_v0_15_item_details/migration.sql
new file mode 100644
index 0000000..0e3caad
--- /dev/null
+++ b/prisma/migrations/20260616052619_v0_15_item_details/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE "Item" ADD COLUMN "insured" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "lifetimeWarranty" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "purchaseFrom" TEXT,
+ADD COLUMN "warrantyDetails" TEXT;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index cce618d..0f67931 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -314,12 +314,16 @@ model Item {
containedItems Item[] @relation("ItemNesting")
// Durable attributes
- value Decimal? @db.Decimal(10, 2)
- purchaseDate DateTime?
- warrantyExpiry DateTime?
- serial String?
- modelNumber String?
- brand String?
+ value Decimal? @db.Decimal(10, 2) // purchase price
+ purchaseDate DateTime?
+ purchaseFrom String? // where it was bought
+ warrantyExpiry DateTime?
+ warrantyDetails String? // freeform warranty terms
+ lifetimeWarranty Boolean @default(false)
+ insured Boolean @default(false)
+ serial String?
+ modelNumber String?
+ brand String? // brand / manufacturer
// Consumable attributes (Pantry phase)
barcode String?
diff --git a/src/app/(dashboard)/inventory/[id]/page.tsx b/src/app/(dashboard)/inventory/[id]/page.tsx
index 72bdf01..a59b2a7 100644
--- a/src/app/(dashboard)/inventory/[id]/page.tsx
+++ b/src/app/(dashboard)/inventory/[id]/page.tsx
@@ -5,13 +5,16 @@ import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import {
ChevronLeft, MapPin, Box, Package, CalendarDays, DollarSign, Hash, Tag, FileText, Barcode,
+ Store, ShieldCheck, Wrench, AlertCircle,
} from "lucide-react";
import { formatDate } from "@/lib/utils";
import { warrantyStatus, WARRANTY_LABELS } from "@/lib/inventory/value";
import { ITEM_LOG_LABELS, ITEM_LOG_COLORS } from "@/lib/inventory/item-fields";
+import { isOverdue, describeRecurrence } from "@/lib/tasks/schedule";
import { EditItemButton, type ItemFields } from "@/components/inventory/item-dialog";
import { DeleteItemButton } from "@/components/inventory/delete-item-button";
import { AddItemLogButton } from "@/components/inventory/add-item-log-button";
+import { ScheduleMaintenanceButton } from "@/components/inventory/schedule-maintenance-button";
import { AttachmentUpload } from "@/components/inventory/attachment-upload";
function toDateInput(d: Date | null): string {
@@ -34,6 +37,7 @@ export default async function ItemDetailPage({ params }: { params: { id: string
attachments: { orderBy: { createdAt: "asc" } },
logs: { orderBy: { date: "desc" } },
containedItems: { where: { active: true }, orderBy: { name: "asc" }, select: { id: true, name: true } },
+ tasks: { where: { active: true }, orderBy: { nextDue: "asc" } },
},
}),
db.location.findMany({ where: { active: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
@@ -43,7 +47,8 @@ export default async function ItemDetailPage({ params }: { params: { id: string
if (!item || !item.active) notFound();
- const wStatus = warrantyStatus(item.warrantyExpiry, new Date());
+ const wStatus = item.lifetimeWarranty ? "active" : warrantyStatus(item.warrantyExpiry, new Date());
+ const warrantyLabel = item.lifetimeWarranty ? "Lifetime warranty" : WARRANTY_LABELS[wStatus];
const photos = item.attachments.filter((a) => a.kind === "PHOTO");
const docs = item.attachments.filter((a) => a.kind !== "PHOTO");
@@ -60,7 +65,11 @@ export default async function ItemDetailPage({ params }: { params: { id: string
serial: item.serial ?? "",
value: item.value != null ? String(Number(item.value)) : "",
purchaseDate: toDateInput(item.purchaseDate),
+ purchaseFrom: item.purchaseFrom ?? "",
warrantyExpiry: toDateInput(item.warrantyExpiry),
+ warrantyDetails: item.warrantyDetails ?? "",
+ lifetimeWarranty: item.lifetimeWarranty,
+ insured: item.insured,
barcode: item.barcode ?? "",
notes: item.notes ?? "",
labelIds: item.labels.map((l) => l.labelId),
@@ -72,6 +81,9 @@ export default async function ItemDetailPage({ params }: { params: { id: string
if (Number(item.quantity) !== 1) facts.push({ icon: , label: "Quantity", value: `${Number(item.quantity)}${item.unit ? ` ${item.unit}` : ""}` });
if (item.value != null) facts.push({ icon: , label: "Value", value: Number(item.value).toLocaleString(undefined, { style: "currency", currency: "USD" }) });
if (item.purchaseDate) facts.push({ icon: , label: "Purchased", value: formatDate(item.purchaseDate) });
+ if (item.purchaseFrom) facts.push({ icon: , label: "From", value: item.purchaseFrom });
+ if (item.lifetimeWarranty || item.warrantyExpiry) facts.push({ icon: , label: "Warranty", value: item.lifetimeWarranty ? "Lifetime" : `until ${formatDate(item.warrantyExpiry!)}` });
+ if (item.insured) facts.push({ icon: , label: "Insured", value: "Yes" });
if (item.brand) facts.push({ icon: , label: "Brand", value: item.brand });
if (item.modelNumber) facts.push({ icon: , label: "Model", value: item.modelNumber });
if (item.serial) facts.push({ icon: , label: "Serial", value: item.serial });
@@ -87,14 +99,15 @@ export default async function ItemDetailPage({ params }: { params: { id: string
{item.name}
{item.description && {item.description}
}
- {wStatus !== "none" && (
- {WARRANTY_LABELS[wStatus]}
+ {(item.lifetimeWarranty || wStatus !== "none") && (
+ {warrantyLabel}
)}
i.id !== item.id)} labels={labels} />
+
@@ -134,6 +147,44 @@ export default async function ItemDetailPage({ params }: { params: { id: string
{item.notes}
)}
+ {item.warrantyDetails && (
+
+
+ Warranty
+ {item.warrantyDetails}
+
+
+ )}
+
+
+
+
Maintenance
+
+ {item.tasks.length === 0 ? (
+
+ No maintenance scheduled. Use “Schedule maintenance” above to set a recurring reminder.
+
+ ) : (
+
+ {item.tasks.map((t) => {
+ const overdue = isOverdue(t.nextDue);
+ return (
+
+
+
{t.title}
+ · {describeRecurrence(t.recurrence, t.intervalDays, t.month)}
+
+
+ {overdue && }
+ {overdue ? "Overdue " : "Due "}{formatDate(t.nextDue)}
+
+
+ );
+ })}
+
+ )}
+
+
{docs.length > 0 && (
Documents
diff --git a/src/app/(dashboard)/locations/page.tsx b/src/app/(dashboard)/locations/page.tsx
new file mode 100644
index 0000000..d1ed4f3
--- /dev/null
+++ b/src/app/(dashboard)/locations/page.tsx
@@ -0,0 +1,35 @@
+import { db } from "@/lib/db";
+import { LocationManager } from "@/components/locations/location-manager";
+
+export const metadata = { title: "Locations" };
+
+export default async function LocationsPage() {
+ const locations = await db.location.findMany({
+ where: { active: true },
+ orderBy: { name: "asc" },
+ select: {
+ id: true,
+ name: true,
+ kind: true,
+ parentId: true,
+ _count: {
+ select: {
+ plants: { where: { active: true } },
+ items: { where: { active: true } },
+ },
+ },
+ },
+ });
+
+ return (
+
+
+
Locations
+
+ Manage the places on your property — nest them as deep as you like.
+
+
+
+
+ );
+}
diff --git a/src/app/api/v1/items/[id]/attachments/route.ts b/src/app/api/v1/items/[id]/attachments/route.ts
index 9565902..0c4129c 100644
--- a/src/app/api/v1/items/[id]/attachments/route.ts
+++ b/src/app/api/v1/items/[id]/attachments/route.ts
@@ -6,7 +6,7 @@ import { authenticateRequest, handleApiError, ApiError } from "@/lib/api/authent
export const dynamic = "force-dynamic";
-const KINDS = new Set(["PHOTO", "MANUAL", "RECEIPT"]);
+const KINDS = new Set(["PHOTO", "MANUAL", "RECEIPT", "WARRANTY"]);
// POST /api/v1/items/:id/attachments — multipart upload (file, kind). Files land
// under public/uploads/items (a persistent Docker volume), same as suggestions.
diff --git a/src/components/inventory/attachment-upload.tsx b/src/components/inventory/attachment-upload.tsx
index 36a8532..7ec9383 100644
--- a/src/components/inventory/attachment-upload.tsx
+++ b/src/components/inventory/attachment-upload.tsx
@@ -3,33 +3,61 @@
import { useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
import { useToast } from "@/hooks/use-toast";
-import { ImagePlus } from "lucide-react";
+import { Paperclip, ChevronDown } from "lucide-react";
+
+const KINDS: { value: string; label: string; accept: string }[] = [
+ { value: "PHOTO", label: "Photo", accept: "image/*" },
+ { value: "MANUAL", label: "Manual", accept: ".pdf,image/*" },
+ { value: "RECEIPT", label: "Receipt", accept: ".pdf,image/*" },
+ { value: "WARRANTY", label: "Warranty doc", accept: ".pdf,image/*" },
+];
export function AttachmentUpload({ itemId }: { itemId: string }) {
const [busy, setBusy] = useState(false);
+ const [kind, setKind] = useState("PHOTO");
const inputRef = useRef
(null);
const router = useRouter();
const { toast } = useToast();
+ function pick(k: string) {
+ setKind(k);
+ // Let state settle, then open the picker with the right accept filter.
+ requestAnimationFrame(() => inputRef.current?.click());
+ }
+
async function onFile(file: File) {
setBusy(true);
const form = new FormData();
form.append("file", file);
- form.append("kind", "PHOTO");
+ form.append("kind", kind);
const res = await fetch(`/api/v1/items/${itemId}/attachments`, { method: "POST", body: form });
setBusy(false);
- if (res.ok) { toast({ title: "Photo added" }); router.refresh(); }
+ if (res.ok) { toast({ title: "Attached" }); router.refresh(); }
else toast({ title: "Error", description: "Upload failed.", variant: "destructive" });
}
+ const accept = KINDS.find((k) => k.value === kind)?.accept ?? "*";
+
return (
<>
- { const f = e.target.files?.[0]; if (f) onFile(f); e.target.value = ""; }} />
-
+
+
+
+
+
+ {KINDS.map((k) => (
+ pick(k.value)}>{k.label}
+ ))}
+
+
>
);
}
diff --git a/src/components/inventory/item-dialog.tsx b/src/components/inventory/item-dialog.tsx
index 85fac6d..1f6901d 100644
--- a/src/components/inventory/item-dialog.tsx
+++ b/src/components/inventory/item-dialog.tsx
@@ -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({
- set("warrantyExpiry", e.target.value)} />
+ set("warrantyExpiry", e.target.value)} disabled={f.lifetimeWarranty} />
+
+
+
+
+ set("purchaseFrom", e.target.value)} placeholder="Home Depot, Amazon, gift…" />
+
+
+
+
+
+
+
diff --git a/src/components/inventory/schedule-maintenance-button.tsx b/src/components/inventory/schedule-maintenance-button.tsx
new file mode 100644
index 0000000..173bb53
--- /dev/null
+++ b/src/components/inventory/schedule-maintenance-button.tsx
@@ -0,0 +1,108 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
+} from "@/components/ui/dialog";
+import {
+ Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
+} from "@/components/ui/select";
+import { useToast } from "@/hooks/use-toast";
+import { Wrench } from "lucide-react";
+import { MONTH_NAMES } from "@/lib/tasks/schedule";
+
+export function ScheduleMaintenanceButton({ itemId, itemName }: { itemId: string; itemName: string }) {
+ const [open, setOpen] = useState(false);
+ const [title, setTitle] = useState(`Maintain ${itemName}`);
+ const [recurrence, setRecurrence] = useState("YEARLY");
+ const [intervalDays, setIntervalDays] = useState("90");
+ const [month, setMonth] = useState("1");
+ const [nextDue, setNextDue] = useState(new Date().toISOString().slice(0, 10));
+ const [busy, setBusy] = useState(false);
+ const router = useRouter();
+ const { toast } = useToast();
+
+ async function submit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!title.trim()) return;
+ const body: Record
= {
+ title: title.trim(),
+ category: "EQUIPMENT",
+ itemId,
+ recurrence,
+ nextDue: new Date(nextDue).toISOString(),
+ };
+ if (recurrence === "CUSTOM") body.intervalDays = parseInt(intervalDays);
+ if (recurrence === "YEARLY") body.month = parseInt(month);
+
+ setBusy(true);
+ const res = await fetch("/api/v1/tasks", {
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
+ });
+ setBusy(false);
+ if (!res.ok) { toast({ title: "Error", description: "Could not schedule.", variant: "destructive" }); return; }
+ toast({ title: "Maintenance scheduled", description: "It'll show in Reminders too." });
+ setOpen(false);
+ router.refresh();
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx
index 2839926..737ce4e 100644
--- a/src/components/layout/sidebar.tsx
+++ b/src/components/layout/sidebar.tsx
@@ -2,7 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { Leaf, Home, Wrench, LayoutDashboard, Bell, HardDrive, Lightbulb, RefreshCw, BookOpen, KeyRound, Package, PawPrint } from "lucide-react";
+import { Leaf, Home, Wrench, LayoutDashboard, Bell, HardDrive, Lightbulb, RefreshCw, BookOpen, KeyRound, Package, PawPrint, MapPin } from "lucide-react";
import { cn } from "@/lib/utils";
const navItems = [
@@ -11,6 +11,7 @@ const navItems = [
{ label: "Plant types", href: "/garden/types", icon: BookOpen },
{ label: "Inventory", href: "/inventory", icon: Package },
{ label: "Animals", href: "/animals", icon: PawPrint },
+ { label: "Locations", href: "/locations", icon: MapPin },
{ label: "Reminders", href: "/tasks", icon: Bell },
// Home maintenance and Equipment come in future releases
// { label: "Home", href: "/home", icon: Home },
diff --git a/src/components/locations/location-manager.tsx b/src/components/locations/location-manager.tsx
new file mode 100644
index 0000000..373fe94
--- /dev/null
+++ b/src/components/locations/location-manager.tsx
@@ -0,0 +1,176 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Badge } from "@/components/ui/badge";
+import {
+ Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
+} from "@/components/ui/dialog";
+import {
+ Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
+} from "@/components/ui/select";
+import {
+ DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator,
+} from "@/components/ui/dropdown-menu";
+import { useToast } from "@/hooks/use-toast";
+import { Plus, MoreVertical, MapPin, Building2, DoorOpen, Box, Trees, HelpCircle } from "lucide-react";
+import { buildTree, flattenForSelect } from "@/lib/locations/tree";
+
+type Loc = {
+ id: string;
+ name: string;
+ kind: string;
+ parentId: string | null;
+ _count: { plants: number; items: number };
+};
+
+const KIND_LABELS: Record = {
+ AREA: "Area", BUILDING: "Building", ROOM: "Room", STORAGE: "Storage", OTHER: "Other",
+};
+const KIND_ICONS: Record = {
+ AREA: Trees, BUILDING: Building2, ROOM: DoorOpen, STORAGE: Box, OTHER: HelpCircle,
+};
+
+const NONE = "__none__";
+
+export function LocationManager({ locations }: { locations: Loc[] }) {
+ const [dialog, setDialog] = useState(null);
+ const [busy, setBusy] = useState(false);
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const tree = buildTree(locations);
+ const flat = flattenForSelect(tree); // depth-ordered for indentation
+
+ function openAdd(parentId?: string) {
+ setDialog({ mode: "add", name: "", kind: "AREA", parentId: parentId ?? "" });
+ }
+ function openEdit(loc: Loc) {
+ setDialog({ mode: "edit", id: loc.id, name: loc.name, kind: loc.kind, parentId: loc.parentId ?? "" });
+ }
+
+ async function save() {
+ if (!dialog || !dialog.name.trim()) return;
+ setBusy(true);
+ const body = { name: dialog.name.trim(), kind: dialog.kind, parentId: dialog.parentId || null };
+ const res = await fetch(dialog.mode === "add" ? "/api/locations" : `/api/locations/${dialog.id}`, {
+ method: dialog.mode === "add" ? "POST" : "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+ setBusy(false);
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({}));
+ toast({ title: "Error", description: err.error ?? "Could not save.", variant: "destructive" });
+ return;
+ }
+ toast({ title: dialog.mode === "add" ? "Location added" : "Saved" });
+ setDialog(null);
+ router.refresh();
+ }
+
+ async function remove(loc: Loc) {
+ if (!confirm(`Delete "${loc.name}"?`)) return;
+ const res = await fetch(`/api/locations/${loc.id}`, { method: "DELETE" });
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({}));
+ toast({ title: "Can't delete", description: err.error ?? "Move its contents first.", variant: "destructive" });
+ return;
+ }
+ toast({ title: "Deleted" });
+ router.refresh();
+ }
+
+ // Valid parents for the dialog (exclude self when editing — server guards descendants).
+ const parentChoices = flat.filter((n) => !(dialog?.mode === "edit" && n.id === dialog.id));
+
+ return (
+
+
+
+ The shared place tree — garden areas, buildings, rooms, and storage. Plants and items live here.
+
+
+
+
+ {locations.length === 0 ? (
+
+ ) : (
+
+ {flat.map((n) => {
+ const Icon = KIND_ICONS[n.kind] ?? MapPin;
+ const counts = (n as unknown as Loc)._count;
+ return (
+
+
+
{n.name}
+
{KIND_LABELS[n.kind] ?? n.kind}
+
+ {[counts?.plants ? `${counts.plants} plants` : "", counts?.items ? `${counts.items} items` : ""].filter(Boolean).join(" · ")}
+
+
+
+
+
+
+
+ openAdd(n.id)}>Add sub-location
+ openEdit(n as unknown as Loc)}>Rename / move
+
+ remove(n as unknown as Loc)}>Delete
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/lib/api/schemas.ts b/src/lib/api/schemas.ts
index bee53d4..dbcdbbe 100644
--- a/src/lib/api/schemas.ts
+++ b/src/lib/api/schemas.ts
@@ -14,6 +14,8 @@ export const taskCreateInput = z.object({
notes: z.string().optional(),
category: z.enum(["GARDEN", "HOME", "EQUIPMENT", "OTHER"]).default("GARDEN"),
plantId: z.string().optional(),
+ itemId: z.string().optional(),
+ animalId: z.string().optional(),
recurrence: z.enum(["ONCE", "CUSTOM", "MONTHLY", "YEARLY"]).default("ONCE"),
intervalDays: z.number().int().positive().optional(),
month: z.number().int().min(1).max(12).optional(),
@@ -34,7 +36,11 @@ export const itemCreateInput = z.object({
parentItemId: z.string().nullable().optional(),
value: z.coerce.number().nonnegative().nullable().optional(),
purchaseDate: z.string().nullable().optional(),
+ purchaseFrom: z.string().optional(),
warrantyExpiry: z.string().nullable().optional(),
+ warrantyDetails: z.string().optional(),
+ lifetimeWarranty: z.boolean().optional(),
+ insured: z.boolean().optional(),
serial: z.string().optional(),
modelNumber: z.string().optional(),
brand: z.string().optional(),
diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts
index 52f0a00..4a8145d 100644
--- a/src/lib/changelog.ts
+++ b/src/lib/changelog.ts
@@ -8,6 +8,14 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ version: "0.15.0",
+ date: "2026-06-16",
+ changes: [
+ "Inventory got a lot deeper: attach manuals, receipts, and warranty docs (not just photos) to an item; record where you bought it, warranty details, lifetime-warranty and insured flags; and schedule recurring maintenance right from an item (it shows up in Reminders, with overdue flagged).",
+ "New Locations page in the menu — see and manage your whole place tree in one spot: add, rename, move, or remove garden areas, rooms, and storage, nested as deep as you like.",
+ ],
+ },
{
version: "0.14.0",
date: "2026-06-16",
diff --git a/src/lib/services/items.ts b/src/lib/services/items.ts
index 920cfed..7da232d 100644
--- a/src/lib/services/items.ts
+++ b/src/lib/services/items.ts
@@ -82,7 +82,11 @@ export async function createItem(ctx: AuthContext, input: ItemCreateInput) {
parentItemId: r.parentItemId || null,
value: r.value ?? null,
purchaseDate: toDate(r.purchaseDate),
+ purchaseFrom: r.purchaseFrom?.trim() || null,
warrantyExpiry: toDate(r.warrantyExpiry),
+ warrantyDetails: r.warrantyDetails?.trim() || null,
+ lifetimeWarranty: r.lifetimeWarranty ?? false,
+ insured: r.insured ?? false,
serial: r.serial?.trim() || null,
modelNumber: r.modelNumber?.trim() || null,
brand: r.brand?.trim() || null,
@@ -121,7 +125,11 @@ export async function updateItem(ctx: AuthContext, id: string, input: ItemUpdate
if (r.parentItemId !== undefined) data.parentItem = r.parentItemId ? { connect: { id: r.parentItemId } } : { disconnect: true };
if (r.value !== undefined) data.value = r.value;
if (r.purchaseDate !== undefined) data.purchaseDate = toDate(r.purchaseDate);
+ if (r.purchaseFrom !== undefined) data.purchaseFrom = r.purchaseFrom?.trim() || null;
if (r.warrantyExpiry !== undefined) data.warrantyExpiry = toDate(r.warrantyExpiry);
+ if (r.warrantyDetails !== undefined) data.warrantyDetails = r.warrantyDetails?.trim() || null;
+ if (r.lifetimeWarranty !== undefined) data.lifetimeWarranty = r.lifetimeWarranty;
+ if (r.insured !== undefined) data.insured = r.insured;
if (r.serial !== undefined) data.serial = r.serial?.trim() || null;
if (r.modelNumber !== undefined) data.modelNumber = r.modelNumber?.trim() || null;
if (r.brand !== undefined) data.brand = r.brand?.trim() || null;
diff --git a/src/lib/services/tasks.ts b/src/lib/services/tasks.ts
index 35a044a..f927ee8 100644
--- a/src/lib/services/tasks.ts
+++ b/src/lib/services/tasks.ts
@@ -18,6 +18,8 @@ export async function createTask(ctx: AuthContext, input: TaskCreateInput) {
notes: input.notes?.trim() || null,
category: input.category,
plantId: input.plantId || null,
+ itemId: input.itemId || null,
+ animalId: input.animalId || null,
recurrence: input.recurrence,
intervalDays: input.intervalDays ?? null,
month: input.month ?? null,
diff --git a/src/lib/version.ts b/src/lib/version.ts
index 8e9360e..7f7cd3a 100644
--- a/src/lib/version.ts
+++ b/src/lib/version.ts
@@ -1,2 +1,2 @@
// Bump on every user-visible release. Changelog entries live in `src/lib/changelog.ts`.
-export const APP_VERSION = "0.14.0";
+export const APP_VERSION = "0.15.0";