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>
This commit is contained in:
68
src/lib/services/location-setup.ts
Normal file
68
src/lib/services/location-setup.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// Bulk Location setup — create a nested tree from a pasted outline, and
|
||||
// (optionally) sweep existing top-level garden areas under a parent. DB-touching;
|
||||
// parsing is pure (src/lib/locations/outline.ts).
|
||||
import { db } from "@/lib/db";
|
||||
import type { AuthContext } from "@/lib/api/authenticate";
|
||||
import { recomputeAllPaths } from "@/lib/locations/db";
|
||||
import { parseLocationOutline } from "@/lib/locations/outline";
|
||||
|
||||
export type SetupSummary = { created: number; moved: number };
|
||||
|
||||
export async function bulkSetupLocations(
|
||||
ctx: AuthContext,
|
||||
opts: { outline: string; groupGardenUnder?: string },
|
||||
): Promise<SetupSummary> {
|
||||
const nodes = parseLocationOutline(opts.outline);
|
||||
|
||||
// find-or-create by (name, parentId), case-insensitive.
|
||||
const rows = await db.location.findMany({ select: { id: true, name: true, parentId: true } });
|
||||
const cache = new Map<string, string>();
|
||||
for (const r of rows) cache.set(`${r.parentId ?? ""}:${r.name.toLowerCase()}`, r.id);
|
||||
|
||||
let created = 0;
|
||||
const stack: { depth: number; id: string }[] = []; // ancestor chain
|
||||
for (const node of nodes) {
|
||||
while (stack.length && stack[stack.length - 1].depth >= node.depth) stack.pop();
|
||||
const parentId = stack.length ? stack[stack.length - 1].id : null;
|
||||
const key = `${parentId ?? ""}:${node.name.toLowerCase()}`;
|
||||
let id = cache.get(key);
|
||||
if (!id) {
|
||||
const loc = await db.location.create({ data: { name: node.name, kind: "OTHER", parentId } });
|
||||
id = loc.id;
|
||||
cache.set(key, id);
|
||||
created++;
|
||||
}
|
||||
stack.push({ depth: node.depth, id });
|
||||
}
|
||||
|
||||
// Sweep existing top-level places that hold plants under the named parent.
|
||||
let moved = 0;
|
||||
if (opts.groupGardenUnder) {
|
||||
const parent = await db.location.findFirst({
|
||||
where: { active: true, parentId: null, name: { equals: opts.groupGardenUnder, mode: "insensitive" } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (parent) {
|
||||
const gardenAreas = await db.location.findMany({
|
||||
where: {
|
||||
active: true,
|
||||
parentId: null,
|
||||
id: { not: parent.id },
|
||||
plants: { some: { active: true } },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
for (const a of gardenAreas) {
|
||||
await db.location.update({ where: { id: a.id }, data: { parentId: parent.id } });
|
||||
}
|
||||
moved = gardenAreas.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (created > 0 || moved > 0) await recomputeAllPaths();
|
||||
await db.auditEvent.create({
|
||||
data: { action: "location.bulk_setup", actorId: ctx.userId, payload: { created, moved, via: ctx.via } },
|
||||
});
|
||||
|
||||
return { created, moved };
|
||||
}
|
||||
@@ -7,7 +7,12 @@ export function listTasks(_ctx: AuthContext) {
|
||||
return db.task.findMany({
|
||||
where: { active: true },
|
||||
orderBy: { nextDue: "asc" },
|
||||
include: { plant: { select: { id: true, commonName: true } } },
|
||||
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 } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,6 +25,7 @@ export async function createTask(ctx: AuthContext, input: TaskCreateInput) {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user