#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>
76 lines
2.3 KiB
TypeScript
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 };
|
|
}
|