feat: garden dashboard widgets and manifest completion (task 75)

- getCareDueWidgetRows query: schedule rows due within N days, filtered
  by optional container list, ordered by next_due_at
- getGardenOverviewStats query: plant/container/overdue counts + next care
- CareDueWidget: compact list sorted by urgency, inline log-care button,
  truncates at 10 rows with View all link
- GardenOverviewWidget: stat row (plants, containers, overdue) + next care
  line + garden link
- plant-widget.tsx server component: InlineLogButton uses form action to
  call logCare without client JS or navigation
- manifest: garden.care-due and garden.overview widgets registered;
  resolveConfigOptions returns container list for care-due config picker
- quickAdds: icons updated (leaf/box/droplets), log-care URL ->
  /garden?logCare=1
This commit is contained in:
ginnoir
2026-06-01 22:43:53 -05:00
parent 080d26816e
commit 6b25d67537
3 changed files with 307 additions and 6 deletions
+58 -6
View File
@@ -1,11 +1,35 @@
import type { ModuleManifest } from "../_core/module";
import { z } from "zod";
import type { ModuleManifest, WidgetContext } from "../_core/module";
import {
loadContainerForShare,
loadPlantForShare,
type ContainerShareData,
type PlantShareData,
} from "./server/share-queries";
import { searchContainers, searchPlants } from "./server/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 <CareDueWidget rows={rows} />;
}
async function GardenOverviewWidgetServer({ ctx }: { config: unknown; ctx: WidgetContext }) {
const stats = await getGardenOverviewStats(ctx.householdId);
return <GardenOverviewWidget stats={stats} />;
}
const gardenManifest: ModuleManifest = {
id: "garden",
@@ -118,25 +142,53 @@ const gardenManifest: ModuleManifest = {
},
},
],
dashboardWidgets: [],
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) => <CareDueWidgetServer {...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) => <GardenOverviewWidgetServer {...props} />,
},
],
quickAdds: [
{
id: "garden.add-plant",
label: "Add plant",
icon: "sprout",
icon: "leaf",
url: "/garden/plants/new",
},
{
id: "garden.add-container",
label: "Add container",
icon: "sprout",
icon: "box",
url: "/garden/containers/new",
},
{
id: "garden.log-care",
label: "Log plant care",
icon: "droplets",
url: "/garden",
url: "/garden?logCare=1",
},
],
};