feat: voeg organizeSql-optie toe voor gecategoriseerde catalog_pages met Nederlandse namen
CI / check (push) Successful in 23s
CI / deploy (push) Successful in 47s
CI / release (push) Skipped

This commit is contained in:
openhands committed 2026-08-04 16:17:20 +02:00
1 parent 9a0bb4c53b
commit 2c2e9749ca
2 files changed
+92 -6

No files matched your search

+5 -1
View File
@@ -125,6 +125,7 @@ export interface CatalogAuditOptions {
repairNitros?: boolean; repairNitros?: boolean;
sql?: boolean; sql?: boolean;
applySql?: boolean; applySql?: boolean;
organizeSql?: boolean;
repairFurniData?: boolean; repairFurniData?: boolean;
repairStructure?: boolean; repairStructure?: boolean;
} }
@@ -316,6 +317,7 @@ export async function runCatalogAudit(
classname: item.item_name, classname: item.item_name,
itemId: item.id, itemId: item.id,
itemType: item.type, itemType: item.type,
publicName: item.public_name,
}); });
} }
} }
@@ -652,7 +654,9 @@ export async function runCatalogAudit(
message: `Generating catalog SQL for ${missingCatalogEntries.length} items…`, message: `Generating catalog SQL for ${missingCatalogEntries.length} items…`,
}); });
try { try {
const result = await generateCatalogSql(missingCatalogEntries); const result = await generateCatalogSql(missingCatalogEntries, {
organizeSql: options?.organizeSql,
});
catalogSqlCount = result.count; catalogSqlCount = result.count;
onEvent?.({ onEvent?.({
type: "sql_ready", type: "sql_ready",
+87 -5
View File
@@ -7,13 +7,17 @@ import {
} from "@/lib/services/furni-data"; } from "@/lib/services/furni-data";
import { import {
autoPriceFurni, autoPriceFurni,
CATEGORY_PAGE,
classifyFurni,
getOrCreateCategoryPage, getOrCreateCategoryPage,
getOrCreateImportedParentPage,
} from "@/lib/services/furni-import"; } from "@/lib/services/furni-import";
export interface MissingCatalogEntry { export interface MissingCatalogEntry {
classname: string; classname: string;
itemId: number; itemId: number;
itemType: string; itemType: string;
publicName?: string;
} }
export interface CatalogSqlResult { export interface CatalogSqlResult {
@@ -26,14 +30,45 @@ function escSqlLiteral(value: string): string {
return value.replace(/'/g, "''"); 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<string, string> = {
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 * Build standalone `catalog_items` INSERT statements for items_base entries
* that exist (with assets) but have no catalog entry yet. Ids and prices are * 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 * 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. * 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( export async function generateCatalogSql(
entries: MissingCatalogEntry[], entries: MissingCatalogEntry[],
opts: { organizeSql?: boolean } = {},
): Promise<CatalogSqlResult> { ): Promise<CatalogSqlResult> {
const statements: string[] = [ const statements: string[] = [
"-- AtomCMS catalog repair", "-- AtomCMS catalog repair",
@@ -42,6 +77,20 @@ export async function generateCatalogSql(
"START TRANSACTION;", "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 // The generation pass does not INSERT, so MAX(id) would never advance and
// per-entry allocateCatalogItemId would hand out the same id every time. // per-entry allocateCatalogItemId would hand out the same id every time.
// Read the current max once and hand out sequential ids from there. // 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]; `)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
let nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1; 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<string, number>();
let generated = 0; let generated = 0;
for (const entry of entries) { for (const entry of entries) {
let pageId: number; let pageId: number;
try { if (organizeSql && parentPageId) {
pageId = await getOrCreateCategoryPage(entry.classname, entry.itemType); const catKey = classifyFurni(entry.classname, entry.itemType);
} catch { const captionSave = `imp_${catKey}`;
continue; 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 price = autoPriceFurni(entry.classname);
const displayName = entry.publicName || entry.classname;
statements.push( 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++; nextId++;
generated++; generated++;