diff --git a/src/lib/services/catalog-audit.ts b/src/lib/services/catalog-audit.ts index 07adcdc4..08bf8280 100644 --- a/src/lib/services/catalog-audit.ts +++ b/src/lib/services/catalog-audit.ts @@ -125,6 +125,7 @@ export interface CatalogAuditOptions { repairNitros?: boolean; sql?: boolean; applySql?: boolean; + organizeSql?: boolean; repairFurniData?: boolean; repairStructure?: boolean; } @@ -316,6 +317,7 @@ export async function runCatalogAudit( classname: item.item_name, itemId: item.id, itemType: item.type, + publicName: item.public_name, }); } } @@ -652,7 +654,9 @@ export async function runCatalogAudit( message: `Generating catalog SQL for ${missingCatalogEntries.length} items…`, }); try { - const result = await generateCatalogSql(missingCatalogEntries); + const result = await generateCatalogSql(missingCatalogEntries, { + organizeSql: options?.organizeSql, + }); catalogSqlCount = result.count; onEvent?.({ type: "sql_ready", diff --git a/src/lib/services/catalog-repair.ts b/src/lib/services/catalog-repair.ts index ccca31e3..63666dca 100644 --- a/src/lib/services/catalog-repair.ts +++ b/src/lib/services/catalog-repair.ts @@ -7,13 +7,17 @@ import { } from "@/lib/services/furni-data"; import { autoPriceFurni, + CATEGORY_PAGE, + classifyFurni, getOrCreateCategoryPage, + getOrCreateImportedParentPage, } from "@/lib/services/furni-import"; export interface MissingCatalogEntry { classname: string; itemId: number; itemType: string; + publicName?: string; } export interface CatalogSqlResult { @@ -26,14 +30,45 @@ function escSqlLiteral(value: string): string { return value.replace(/'/g, "''"); } +/** + * Derive a human-readable Dutch page caption from a furniture classname that + * follows an event+year pattern (e.g. `xmas2025`, `hween2024`, `val_2025`). + * Falls back to `fallback` when no pattern matches. + */ +function derivePageCaption(classname: string, fallback: string): string { + const m = classname.match( + /^(xmas|hween|easter|val_|valentine|hw_|newyear|summer|carnival)(\d{4})/i, + ); + if (!m) return fallback; + const event = m[1].toLowerCase(); + const year = m[2]; + const eventNames: Record = { + xmas: "Kerst", + hween: "Halloween", + easter: "Pasen", + val_: "Valentijn", + valentine: "Valentijn", + hw_: "Halloween", + newyear: "Nieuwjaar", + summer: "Zomer", + carnival: "Carnaval", + }; + return `${eventNames[event] ?? event} ${year}`; +} + /** * Build standalone `catalog_items` INSERT statements for items_base entries * that exist (with assets) but have no catalog entry yet. Ids and prices are * resolved the same way the app's own imports do it (autoPriceFurni + category * page + next catalog id), so running the SQL matches an app-managed import. + * + * When `organizeSql` is true, the output also contains `catalog_pages` INSERT + * statements that organise items into sub-pages under "Imported Furniture", + * using derived Dutch captions by event+year (e.g. "Kerst 2025"). */ export async function generateCatalogSql( entries: MissingCatalogEntry[], + opts: { organizeSql?: boolean } = {}, ): Promise { const statements: string[] = [ "-- AtomCMS catalog repair", @@ -42,6 +77,20 @@ export async function generateCatalogSql( "START TRANSACTION;", ]; + const { organizeSql = false } = opts; + + // When organizing, we also emit catalog_pages INSERTs for new sub-pages. + // Read the Imported Furniture parent page id and current max page id once. + let parentPageId: number | null = null; + let nextPageId = 0; + if (organizeSql) { + parentPageId = await getOrCreateImportedParentPage(); + const [maxPageResult] = (await db.execute(sql` + SELECT MAX(id) as maxId FROM catalog_pages + `)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown]; + nextPageId = Number(maxPageResult[0]?.maxId ?? 0) + 1; + } + // The generation pass does not INSERT, so MAX(id) would never advance and // per-entry allocateCatalogItemId would hand out the same id every time. // Read the current max once and hand out sequential ids from there. @@ -50,17 +99,50 @@ export async function generateCatalogSql( `)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown]; let nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1; + // Build a map of caption_save → pageId so multiple items in the same + // category share one page (and we emit the page INSERT only once). + const pageMap = new Map(); + let generated = 0; for (const entry of entries) { let pageId: number; - try { - pageId = await getOrCreateCategoryPage(entry.classname, entry.itemType); - } catch { - continue; + if (organizeSql && parentPageId) { + const catKey = classifyFurni(entry.classname, entry.itemType); + const captionSave = `imp_${catKey}`; + const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other; + const caption = derivePageCaption( + entry.classname, + entry.publicName ?? catInfo.label, + ); + let existing = pageMap.get(captionSave); + if (!existing) { + const [found] = (await db.execute(sql` + SELECT id FROM catalog_pages WHERE caption_save = ${captionSave} AND parent_id = ${parentPageId} + `)) as unknown as [Array<{ id: number }>, unknown]; + if (found?.[0]?.id) { + pageMap.set(captionSave, found[0].id); + } else { + const newId = nextPageId++; + pageMap.set(captionSave, newId); + statements.push( + `INSERT INTO catalog_pages (id, caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser) VALUES (${newId}, '${escSqlLiteral(captionSave)}', '${escSqlLiteral(caption)}', 'default_3x3', ${parentPageId}, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '');`, + ); + } + existing = pageMap.get(captionSave); + } + if (!existing) continue; + pageId = existing; + } else { + try { + pageId = await getOrCreateCategoryPage(entry.classname, entry.itemType); + } catch { + continue; + } } const price = autoPriceFurni(entry.classname); + const displayName = entry.publicName || entry.classname; statements.push( - `INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata) VALUES (${nextId}, ${pageId}, ${entry.itemId}, '${escSqlLiteral(entry.classname)}', ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '');`, + `INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata) VALUES (${nextId}, ${pageId}, ${entry.itemId}, '${escSqlLiteral(displayName)}', ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '');`, ); nextId++; generated++;