Files
Moonbase/src/lib/services/location-setup.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

69 lines
2.5 KiB
TypeScript

// 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 };
}