Log care
diff --git a/src/modules/garden/components/push-overdue-button.tsx b/src/modules/garden/components/push-overdue-button.tsx
new file mode 100644
index 0000000..884fb37
--- /dev/null
+++ b/src/modules/garden/components/push-overdue-button.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { useEffect, useState, useTransition } from "react";
+import { pushOverdueToTaskList } from "../server/actions";
+
+export function PushOverdueButton() {
+ const [message, setMessage] = useState(null);
+ const [isError, setIsError] = useState(false);
+ const [isPending, startTransition] = useTransition();
+
+ useEffect(() => {
+ if (!message) return;
+ const id = setTimeout(() => setMessage(null), 4000);
+ return () => clearTimeout(id);
+ }, [message]);
+
+ function handleClick() {
+ setMessage(null);
+ setIsError(false);
+ startTransition(async () => {
+ try {
+ const { added } = await pushOverdueToTaskList();
+ setMessage(
+ added === 0
+ ? "No overdue care tasks."
+ : `Added ${added} task${added !== 1 ? "s" : ""} to your task list.`,
+ );
+ } catch {
+ setIsError(true);
+ setMessage("Failed to push tasks. Please try again.");
+ }
+ });
+ }
+
+ return (
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+ );
+}
diff --git a/src/modules/garden/server/actions.ts b/src/modules/garden/server/actions.ts
index 1f67f26..a16876a 100644
--- a/src/modules/garden/server/actions.ts
+++ b/src/modules/garden/server/actions.ts
@@ -1,6 +1,6 @@
"use server";
-import { and, eq } from "drizzle-orm";
+import { and, eq, lte, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { db } from "@/lib/db";
@@ -8,7 +8,9 @@ import { getCurrentSession } from "@/lib/session";
import { logActivity } from "@/modules/_core/activity";
import { cancelReminder, scheduleReminder } from "@/modules/_core/reminders";
import { gardenCareLogs, gardenCareSchedules, gardenContainers, gardenPlants } from "../schema";
+import { createCalendarEvent } from "./calendar-bridge";
import { updateScheduleAfterCare } from "./care-schedule";
+import { addListItem, getList, listLists } from "./lists-bridge";
const containerInput = z.object({
name: z.string().trim().min(1).max(120),
@@ -481,3 +483,114 @@ export async function toggleCareSchedule(input: { id: string; enabled: boolean }
revalidatePath(`/garden/plants/${row.plantId}`);
}
+
+// βββ Calendar integration βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function buildCareTitle(careType: string, plantName: string): string {
+ const verbs: Record = {
+ watering: "Water",
+ fertilizing: "Fertilize",
+ repotting: "Repot",
+ pruning: "Prune",
+ };
+ const verb = verbs[careType];
+ return verb ? `${verb} ${plantName}` : `${careType} β ${plantName}`;
+}
+
+const scheduleOnCalendarInput = z.object({
+ scheduleId: z.string().uuid(),
+ calendarId: z.string().uuid(),
+ reminderMinutesBefore: z.number().int().min(0).max(1440).optional(),
+});
+
+export async function scheduleOnCalendar(input: z.input) {
+ const parsed = scheduleOnCalendarInput.parse(input);
+ const { household } = await getCurrentSession();
+
+ const [row] = await db
+ .select({
+ nextDueAt: gardenCareSchedules.nextDueAt,
+ careType: gardenCareSchedules.careType,
+ plantName: gardenPlants.name,
+ })
+ .from(gardenCareSchedules)
+ .innerJoin(gardenPlants, eq(gardenCareSchedules.plantId, gardenPlants.id))
+ .where(
+ and(
+ eq(gardenCareSchedules.id, parsed.scheduleId),
+ eq(gardenCareSchedules.householdId, household.id),
+ ),
+ )
+ .limit(1);
+
+ if (!row) throw new Error("Forbidden");
+ if (!row.nextDueAt) throw new Error("This schedule has no upcoming due date set.");
+
+ const startAt = row.nextDueAt;
+ const endAt = new Date(startAt.getTime() + 30 * 60 * 1000);
+ const title = buildCareTitle(row.careType, row.plantName);
+
+ await createCalendarEvent({
+ calendarId: parsed.calendarId,
+ title,
+ startAt,
+ endAt,
+ allDay: false,
+ notes: `Scheduled from garden. Next due: ${startAt.toLocaleDateString()}`,
+ remindMinutesBefore: parsed.reminderMinutesBefore ?? null,
+ });
+
+ revalidatePath("/calendar");
+}
+
+// βββ Lists integration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export async function pushOverdueToTaskList(): Promise<{ added: number }> {
+ const { household } = await getCurrentSession();
+
+ const overduePairs = await db
+ .select({
+ plantName: gardenPlants.name,
+ careType: gardenCareSchedules.careType,
+ nextDueAt: gardenCareSchedules.nextDueAt,
+ })
+ .from(gardenCareSchedules)
+ .innerJoin(gardenPlants, eq(gardenCareSchedules.plantId, gardenPlants.id))
+ .where(
+ and(
+ eq(gardenCareSchedules.householdId, household.id),
+ eq(gardenCareSchedules.enabled, true),
+ lte(gardenCareSchedules.nextDueAt, sql`now()`),
+ ),
+ );
+
+ if (overduePairs.length === 0) return { added: 0 };
+
+ const taskLists = await listLists({ type: "task" });
+ const taskList = taskLists[0];
+ if (!taskList) return { added: 0 };
+
+ const listDetail = await getList(taskList.id);
+ const existingTexts = new Set(listDetail.items.map((i) => i.text));
+
+ let added = 0;
+ for (const pair of overduePairs) {
+ const title = buildCareTitle(pair.careType, pair.plantName);
+ if (existingTexts.has(title)) continue;
+
+ const daysOverdue = pair.nextDueAt
+ ? Math.abs(Math.floor((Date.now() - pair.nextDueAt.getTime()) / (1000 * 60 * 60 * 24)))
+ : 0;
+
+ await addListItem({
+ listId: taskList.id,
+ text: title,
+ notes: `Overdue by ${daysOverdue} day(s)`,
+ dueAt: pair.nextDueAt,
+ });
+ existingTexts.add(title);
+ added++;
+ }
+
+ return { added };
+}
diff --git a/src/modules/garden/server/calendar-bridge.ts b/src/modules/garden/server/calendar-bridge.ts
new file mode 100644
index 0000000..4598232
--- /dev/null
+++ b/src/modules/garden/server/calendar-bridge.ts
@@ -0,0 +1,3 @@
+export { createEvent as createCalendarEvent } from "@/modules/calendar/server/actions";
+export { listCalendars } from "@/modules/calendar/server/queries";
+export type { CalendarDto } from "@/modules/calendar/server/queries";
diff --git a/src/modules/garden/server/lists-bridge.ts b/src/modules/garden/server/lists-bridge.ts
new file mode 100644
index 0000000..b96676d
--- /dev/null
+++ b/src/modules/garden/server/lists-bridge.ts
@@ -0,0 +1,2 @@
+export { addItem as addListItem } from "@/modules/lists/server/actions";
+export { getList, listLists } from "@/modules/lists/server/queries";