#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>
69 lines
2.5 KiB
TypeScript
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 };
|
|
}
|