# 72 — Garden plants ## Goal Implement full CRUD for individual **plants**. Each plant has a name, category, care notes, health status, up to 10 photos, an optional container assignment, and species data pulled from the **Perenual API** when the user searches for a known species. ## Depends on - 70 (schema, MinIO, upload route), 71 (containers — container assignment picker) ## Scope ### Perenual API wrapper (`src/modules/garden/server/species-lookup.ts`) Perenual (perenual.com) provides a free-tier plant species API. Key endpoint: `GET /api/species-list?key=&q=`. Implement: ```typescript export type SpeciesSuggestion = { id: string; common_name: string; scientific_name: string; watering: string; // e.g. "frequent", "average", "minimum" sunlight: string[]; // e.g. ["full sun", "part shade"] cycle: string; // e.g. "Perennial" default_image_url: string | null; }; export async function searchSpecies(query: string): Promise; export async function getSpeciesById(id: string): Promise; ``` Cache results for 24 hours in a `garden_species_cache` table (columns: `species_id` text pk, `data` jsonb, `cached_at` timestamptz). Add this table to the schema in `src/modules/garden/schema.ts` and include it in the migration from task 70 (or add a new migration). If the API is unreachable or returns an error, log the failure and return `[]` / `null`. `PERENUAL_API_KEY` from env; if absent, `searchSpecies` returns `[]` silently so the app works without the key. ### Server (`src/modules/garden/server/`) Add to `actions.ts`: - `createPlant(input)` — Zod-validated, `getCurrentSession`, insert, `logActivity`, `revalidatePath("/garden")`. - `updatePlant(input: { id, ...partials })` — patch, household check, logActivity. - `deletePlant(input: { id })` — household check, logActivity; care logs + schedules cascade-delete via DB. - `addPlantImage(input: { id, url })` — appends URL to `images` jsonb array; reject if already 10 images. - `removePlantImage(input: { id, url })` — removes URL from array; if it was `primary_image_url`, set primary to first remaining or null. - `setPrimaryImage(input: { id, url })` — sets `primary_image_url`; url must already be in `images`. Add to `queries.ts`: - `listPlants({ containerId? })` — all plants in household optionally filtered by container. Include last-care timestamps per type via a lateral subquery. - `getPlant(id)` — full plant data + container name + care log summary (last 5 entries) + active schedules. ### UI (`src/modules/garden/components/`) **`plant-list.tsx`** (server component) - Grouped by container first; "Unassigned" group for plants with no container. - Each card: primary image thumbnail, name, health badge, "last watered X days ago", overdue care indicator. - Replaces the placeholder in the "Plants" tab on `/garden/page.tsx` from task 71. **`plant-detail.tsx`** (server component) - Three-tab layout: **Info**, **Gallery**, **Care** (Care tab filled in by task 73). - Info tab: all fields, container link, health/stage badges, species info. - Gallery tab: photo grid, primary image star-toggle, delete individual image, upload new image. **`plant-form.tsx`** (client component) - Fields: name (required), category (select), container (select, nullable), health status (select), growth stage (select). - Species search combobox: debounced search → `searchSpecies` → select → auto-fills scientific name, sunlight, watering notes. User can override. - Care notes textareas: watering notes, fertilizing notes, general notes. - Acquisition date picker. - Image uploader: drag-and-drop or click, previews, max 10. First uploaded image auto-set as primary. - `containerId` query-param pre-fill (from task 71 "Add plant to container" button). **`species-search.tsx`** (client component) - Debounced combobox calling a `/api/garden/species-search` route handler. Fires `onSelect(suggestion)` on pick. ### Pages (`src/app/garden/`) - `plants/new/page.tsx` — `` in create mode. - `plants/[id]/page.tsx` — `` with edit/delete controls. - `plants/[id]/edit/page.tsx` — `` in edit mode. ### Activity log `renderActivity` for `garden.plant`: - `create` → `Added plant "${name}"` - `update` → `Updated "${name}"` - `delete` → `Removed plant "${name}"` ### Search Register `search` on the `garden.plant` entity using `ilike` on `name || ' ' || coalesce(scientific_name, '')`. ### Share links Plants are shareable (read-only). The shared view shows name, scientific name, primary image, gallery, health status, and care notes. Add `loadPlantForShare(id)` in `src/modules/garden/server/share-queries.ts`. ## Out of scope - Care log UI and schedules (task 73). - Dashboard widgets (task 75). ## Acceptance criteria - [ ] Create / edit / delete plants works end-to-end. - [ ] Species search returns Perenual results and auto-fills form fields on selection. - [ ] App works correctly via manual entry when `PERENUAL_API_KEY` is absent. - [ ] Up to 10 images upload; primary image can be set and changed; individual images can be removed. - [ ] Plant list on `/garden` groups by container with an "Unassigned" bucket. - [ ] `garden.plant` entity is registered with search and share support. - [ ] Activity log entries appear for create / update / delete. - [ ] Plant share link renders the read-only view correctly.