// Import service for HomeBox CSV → Moonbase items. Resolves (find-or-creates) // the nested location path and labels, then creates the items. The parsing is // pure (src/lib/inventory/homebox-import.ts) and shared with the preview UI. import { createId } from "@paralleldrive/cuid2"; import { db } from "@/lib/db"; import type { AuthContext } from "@/lib/api/authenticate"; import { recomputeAllPaths } from "@/lib/locations/db"; import { parseHomeBoxCsv, splitLocationPath, type HomeBoxItem } from "@/lib/inventory/homebox-import"; export type ImportSummary = { created: number; skipped: number; locationsCreated: number; labelsCreated: number; }; async function buildLocationCache() { const rows = await db.location.findMany({ select: { id: true, name: true, parentId: true } }); const cache = new Map(); // `${parentId||''}:${nameLower}` → id for (const r of rows) cache.set(`${r.parentId ?? ""}:${r.name.toLowerCase()}`, r.id); return cache; } async function resolveLocationPath( path: string | null, cache: Map, counter: { n: number }, ): Promise { if (!path) return null; const names = splitLocationPath(path); let parentId: string | null = null; for (const name of names) { const key = `${parentId ?? ""}:${name.toLowerCase()}`; const existing: string | undefined = cache.get(key); let id: string; if (existing) { id = existing; } else { const loc = await db.location.create({ data: { name, kind: "OTHER", parentId } }); id = loc.id; cache.set(key, id); counter.n++; } parentId = id; } return parentId; } async function buildLabelCache() { const rows = await db.label.findMany({ select: { id: true, name: true } }); const cache = new Map(); for (const r of rows) cache.set(r.name.toLowerCase(), r.id); return cache; } async function resolveLabels( names: string[], cache: Map, counter: { n: number }, ): Promise { const ids: string[] = []; for (const name of names) { let id = cache.get(name.toLowerCase()); if (!id) { const label = await db.label.create({ data: { name } }); id = label.id; cache.set(name.toLowerCase(), id); counter.n++; } ids.push(id); } return ids; } function toDate(s: string | null): Date | null { return s ? new Date(s) : null; } export async function importHomeBoxItems( ctx: AuthContext, opts: { csv: string; skip?: string[] }, ): Promise { const parsed = parseHomeBoxCsv(opts.csv); const skip = new Set((opts.skip ?? []).map((s) => s.trim().toLowerCase())); const locCache = await buildLocationCache(); const labelCache = await buildLabelCache(); const locCounter = { n: 0 }; const labelCounter = { n: 0 }; let created = 0; let skipped = 0; for (const hb of parsed as HomeBoxItem[]) { if (skip.has(hb.name.trim().toLowerCase())) { skipped++; continue; } const locationId = await resolveLocationPath(hb.locationPath, locCache, locCounter); const labelIds = await resolveLabels(hb.labels, labelCache, labelCounter); await db.item.create({ data: { name: hb.name, description: hb.description, quantity: hb.quantity || 1, locationId, value: hb.value, purchaseDate: toDate(hb.purchaseDate), purchaseFrom: hb.purchaseFrom, warrantyExpiry: toDate(hb.warrantyExpiry), warrantyDetails: hb.warrantyDetails, lifetimeWarranty: hb.lifetimeWarranty, insured: hb.insured, serial: hb.serial, modelNumber: hb.modelNumber, brand: hb.brand, notes: hb.notes, soldTo: hb.soldTo, soldPrice: hb.soldPrice, soldDate: toDate(hb.soldDate), soldNotes: hb.soldNotes, customFields: Object.keys(hb.customFields).length ? hb.customFields : undefined, active: !hb.archived, qrSlug: createId().slice(0, 10), ...(labelIds.length ? { labels: { create: labelIds.map((labelId) => ({ labelId })) } } : {}), }, }); created++; } // New locations were created without paths — recompute breadcrumbs once. if (locCounter.n > 0) await recomputeAllPaths(); await db.auditEvent.create({ data: { action: "item.import", actorId: ctx.userId, payload: { created, skipped, source: "homebox", via: ctx.via }, }, }); return { created, skipped, locationsCreated: locCounter.n, labelsCreated: labelCounter.n }; }