import { z } from "zod"; import { and, eq, sql } from "drizzle-orm"; import { db } from "@/lib/db"; import { registerItemToggleHook } from "../_core/registry"; import type { ModuleManifest, WidgetContext } from "../_core/module"; import { loadContainerForShare, loadPlantForShare, type ContainerShareData, type PlantShareData, } from "./server/share-queries"; import { listContainers, searchContainers, searchPlants, getCareDueWidgetRows, getGardenOverviewStats, } from "./server/queries"; import { CareDueWidget, GardenOverviewWidget } from "./components/plant-widget"; const careDueConfigSchema = z.object({ containerIds: z.union([z.literal("all"), z.array(z.string().uuid())]), daysAhead: z.number().int().min(0).max(30).default(0), }); async function CareDueWidgetServer({ config, ctx }: { config: unknown; ctx: WidgetContext }) { const parsed = careDueConfigSchema.parse(config); const rows = await getCareDueWidgetRows(ctx.householdId, parsed.containerIds, parsed.daysAhead); return ; } async function GardenOverviewWidgetServer({ ctx }: { config: unknown; ctx: WidgetContext }) { const stats = await getGardenOverviewStats(ctx.householdId); return ; } const gardenManifest: ModuleManifest = { id: "garden", name: "Garden", nav: { href: "/garden", label: "Garden", icon: "sprout" }, entities: [ { type: "garden.plant", label: { singular: "Plant", plural: "Plants" }, share: { canShare: true, defaultCapabilities: ["read"] }, search: { search: searchPlants }, resolveUrl: (id) => `/garden/plants/${id}`, loadForShare: (id) => loadPlantForShare(id), renderSharedView: ({ data }) => { const d = data as PlantShareData; return (
{d.primaryImageUrl && ( )}

{d.name}

{d.scientificName && (

{d.scientificName}

)}

{d.healthStatus} ยท {d.category}

{d.wateringNotes && (

Watering

{d.wateringNotes}

)} {d.fertilizingNotes && (

Fertilizing

{d.fertilizingNotes}

)} {d.notes &&

{d.notes}

} {d.images.length > 1 && (
{d.images.slice(1).map((url) => ( ))}
)}
); }, renderActivity: (entry) => { const name = entry.payload?.name as string | undefined; if (entry.action === "create") return `Added plant${name ? ` "${name}"` : ""}`; if (entry.action === "delete") return `Removed plant${name ? ` "${name}"` : ""}`; return `Updated${name ? ` "${name}"` : " plant"}`; }, }, { type: "garden.container", label: { singular: "Container", plural: "Containers" }, share: { canShare: true, defaultCapabilities: ["read"] }, search: { search: searchContainers }, resolveUrl: (id) => `/garden/containers/${id}`, loadForShare: (id) => loadContainerForShare(id), renderSharedView: ({ data }) => { const d = data as ContainerShareData; return (

{d.name}

{d.type}

{d.locationNotes &&

{d.locationNotes}

} {d.plants.length > 0 && (

Plants

    {d.plants.map((p) => (
  • {p.name} {p.scientificName && ( ({p.scientificName}) )}
  • ))}
)}
); }, renderActivity: (entry) => { const name = entry.payload?.name as string | undefined; if (entry.action === "create") return `Created container${name ? ` "${name}"` : ""}`; if (entry.action === "delete") return "Deleted container"; return `Updated container${name ? ` "${name}"` : ""}`; }, }, ], dashboardWidgets: [ { id: "garden.care-due", title: "Plants needing care", description: "Shows plants with overdue or upcoming care schedules.", category: "Garden", defaultSize: { w: 4, h: 4 }, minSize: { w: 3, h: 2 }, defaultPriority: 40, configSchema: careDueConfigSchema, defaultConfig: { containerIds: "all", daysAhead: 0 }, resolveConfigOptions: async () => ({ containers: (await listContainers()).map((c) => ({ id: c.id, name: c.name })), }), render: (props) => , }, { id: "garden.overview", title: "Garden overview", description: "Plant and container counts with next care summary.", category: "Garden", defaultSize: { w: 3, h: 2 }, minSize: { w: 2, h: 2 }, defaultPriority: 41, configSchema: z.object({}), defaultConfig: {}, render: (props) => , }, ], quickAdds: [ { id: "garden.add-plant", label: "Add plant", icon: "leaf", url: "/garden/plants/new", }, { id: "garden.add-container", label: "Add container", icon: "box", url: "/garden/containers/new", }, { id: "garden.log-care", label: "Log plant care", icon: "droplets", url: "/garden?logCare=1", }, ], }; import { gardenCareLogs, gardenPlants } from "./schema"; registerItemToggleHook("garden.care-task", async ({ metadata, done, userId }) => { if (!done) return; const plantId = metadata.gardenPlantId; const careType = metadata.gardenCareType; if (typeof plantId !== "string" || typeof careType !== "string") return; const [plant] = await db .select({ householdId: gardenPlants.householdId }) .from(gardenPlants) .where(eq(gardenPlants.id, plantId)) .limit(1); if (!plant) return; // Avoid duplicate log if care was already logged within the last 10 minutes const recentLog = await db .select({ id: gardenCareLogs.id }) .from(gardenCareLogs) .where( and( eq(gardenCareLogs.plantId, plantId), eq(gardenCareLogs.careType, careType), sql`${gardenCareLogs.performedAt} > now() - interval '10 minutes'`, ), ) .limit(1); if (recentLog[0]) return; await db.insert(gardenCareLogs).values({ plantId, householdId: plant.householdId, careType, performedBy: userId, notes: "Logged via task list", performedAt: new Date(), }); }); export default gardenManifest;