Files
Moonbase/src/lib/services/tasks.ts
tonym ddc0a60a0c v0.18.0 — Locations Quick setup + place-based reminders (#4 groundwork)
#4 cleanup + Tony's house structure.

- Task.locationId (migration): reminders can attach to a place, not just a
  plant/item/animal. Add one from a location's menu (e.g. clean gutters on House >
  Exterior); the Reminders page + tasks service surface the linked location/item/
  animal.
- Locations "Quick setup": paste/confirm an indented outline → creates the nested
  tree (find-or-create by name+parent, idempotent), pre-filled with the Moon
  homestead house layout. Optional "sweep existing garden areas under Yard" moves
  every top-level place that holds plants under a new Yard parent — the
  'move all the yard stuff to a new top level' ask.
- Pure outline parser (src/lib/locations/outline.ts, +4 tests); bulk setup service
  + /api/locations/bulk. Verified end-to-end: nested tree + paths, garden sweep,
  place reminder.

Yard sub-bucketing deferred per Tony.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 02:33:52 -05:00

76 lines
2.3 KiB
TypeScript

import { db } from "@/lib/db";
import { nextDueDate } from "@/lib/tasks/schedule";
import { ApiError, type AuthContext } from "@/lib/api/authenticate";
import type { TaskCreateInput, TaskCompleteInput } from "@/lib/api/schemas";
export function listTasks(_ctx: AuthContext) {
return db.task.findMany({
where: { active: true },
orderBy: { nextDue: "asc" },
include: {
plant: { select: { id: true, commonName: true } },
item: { select: { id: true, name: true } },
animal: { select: { id: true, name: true } },
location: { select: { id: true, name: true } },
},
});
}
export async function createTask(ctx: AuthContext, input: TaskCreateInput) {
const task = await db.task.create({
data: {
title: input.title.trim(),
notes: input.notes?.trim() || null,
category: input.category,
plantId: input.plantId || null,
itemId: input.itemId || null,
animalId: input.animalId || null,
locationId: input.locationId || null,
recurrence: input.recurrence,
intervalDays: input.intervalDays ?? null,
month: input.month ?? null,
nextDue: new Date(input.nextDue),
},
});
await db.auditEvent.create({
data: {
action: "task.create",
entityId: task.id,
actorId: ctx.userId,
payload: { title: task.title, recurrence: task.recurrence, via: ctx.via },
},
});
return task;
}
export async function completeTask(ctx: AuthContext, id: string, input: TaskCompleteInput) {
const task = await db.task.findUnique({ where: { id } });
if (!task || !task.active) throw new ApiError(404, "Task not found");
const now = new Date();
const next = nextDueDate(task.recurrence, now, task.intervalDays, task.month);
const [completion] = await db.$transaction([
db.taskCompletion.create({
data: { taskId: task.id, doneAt: now, notes: input.notes?.trim() || null, actorId: ctx.userId },
}),
db.task.update({
where: { id: task.id },
data: {
lastDone: now,
nextDue: next ?? task.nextDue,
active: task.recurrence === "ONCE" ? false : true,
},
}),
]);
await db.auditEvent.create({
data: {
action: "task.complete",
entityId: task.id,
actorId: ctx.userId,
payload: { title: task.title, nextDue: next, via: ctx.via },
},
});
return { completion, nextDue: next };
}