Files
CLProject/scripts/generate-cl-seed.cjs
2026-04-26 16:00:43 +02:00

192 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* generate-cl-seed.cjs
*
* Reads both Punchlist_Reference sheets from the Excel templates and writes
* sql/cl-seed.sql containing INSERT statements for:
* - admin_categories (unique category values)
* - admin_sub_categories (unique sub-category per category, with FK)
* - admin_severities (unique severity values, normalised to title-case)
* - admin_cl_records (every valid row skips rows with no severity)
*
* Run inside the Docker app container:
* node /workspace/scripts/generate-cl-seed.cjs
*
* Normalisation rules applied to severities:
* - Leading / trailing whitespace stripped.
* - Each word capitalised (title-case) so that "major", "Major" and
* "MAJOR" all become "Major"; "on hold" / "On hold" both become "On Hold".
* - Rows whose severity cell is empty, null or undefined are skipped entirely.
*/
'use strict';
const XLSX = require('/app/node_modules/xlsx');
const fs = require('fs');
const path = require('path');
// ─── helpers ────────────────────────────────────────────────────────────────
/** Escape a value for use inside a SQL single-quoted string literal.
* - Replaces CR/LF sequences and bare LF with a single space so the SQL
* file stays on one logical line per value (avoids broken multi-line
* string literals that confuse some SQL clients).
* - Escapes embedded single quotes by doubling them.
*/
function esc(v) {
if (v == null) return '';
return String(v)
.replace(/\r\n|\r|\n/g, ' ') // collapse newlines to space
.replace(/'/g, "''"); // escape single quotes
}
/**
* Convert a string to title-case.
* "on hold" → "On Hold" "major" → "Major"
*/
function toTitleCase(str) {
return str
.trim()
.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
}
// ─── load both files ─────────────────────────────────────────────────────────
const FILES = [
'/workspace/cl_template/OVXXX_AURORA_ANT_CL_MW_V3.2_20220502.xlsx',
'/workspace/cl_template/OVXXX_AURORA_CW_QC_MW_V3.2_20220502.xlsx',
];
const allRows = []; // { sort_order, category, sub_category, desc_en, desc_fr, desc_nl, severity }
for (const filePath of FILES) {
const wb = XLSX.readFile(filePath);
const ws = wb.Sheets['Punchlist_Reference'];
const data = XLSX.utils.sheet_to_json(ws, { header: 1 });
// Row 0 is the header; skip it.
for (let i = 1; i < data.length; i++) {
const row = data[i];
// Column A (index 0) must have a numeric sort_order; empty trailing rows skipped.
if (row[0] == null) continue;
const sortOrder = Number(row[0]);
const category = String(row[4] ?? '').trim();
const subCat = String(row[5] ?? '').trim();
const descEn = String(row[6] ?? '').trim();
const descFr = String(row[7] ?? '').trim();
const descNl = String(row[8] ?? '').trim();
const severityRaw = row[9];
// Skip rows that carry no severity value.
if (severityRaw == null || String(severityRaw).trim() === '') {
console.warn(` Skipping row sort_order=${sortOrder}: no severity value`);
continue;
}
const severity = toTitleCase(String(severityRaw));
allRows.push({ sortOrder, category, subCat, descEn, descFr, descNl, severity });
}
}
console.log(`Loaded ${allRows.length} valid rows total.`);
// ─── derive unique lookup sets (insertion-order preserved via Map) ────────────
// Categories: unique by value
const categoriesMap = new Map(); // value → true (just for dedup, order preserved)
for (const r of allRows) {
if (!categoriesMap.has(r.category)) categoriesMap.set(r.category, true);
}
const categories = [...categoriesMap.keys()]; // ordered list
// Sub-categories: unique by (category, subCat)
const subCatMap = new Map(); // "category|||subCat" → { category, subCat }
for (const r of allRows) {
const key = `${r.category}|||${r.subCat}`;
if (!subCatMap.has(key)) subCatMap.set(key, { category: r.category, subCat: r.subCat });
}
const subCats = [...subCatMap.values()];
// Severities: unique by normalised value
const severitiesMap = new Map();
for (const r of allRows) {
if (!severitiesMap.has(r.severity)) severitiesMap.set(r.severity, true);
}
const severities = [...severitiesMap.keys()];
console.log(`Categories: ${categories.length}, SubCategories: ${subCats.length}, Severities: ${severities.length}`);
// ─── build SQL ───────────────────────────────────────────────────────────────
const lines = [];
lines.push('-- ============================================================');
lines.push('-- cl-seed.sql');
lines.push('--');
lines.push('-- Auto-generated from Punchlist_Reference sheets in:');
lines.push('-- OVXXX_AURORA_ANT_CL_MW_V3.2_20220502.xlsx');
lines.push('-- OVXXX_AURORA_CW_QC_MW_V3.2_20220502.xlsx');
lines.push('--');
lines.push('-- Generated: ' + new Date().toISOString());
lines.push('--');
lines.push('-- Severity values are normalised to title-case.');
lines.push('-- Records with no severity are omitted.');
lines.push('-- Use INSERT IGNORE so re-running the script is idempotent.');
lines.push('-- ============================================================');
lines.push('');
lines.push('USE check_list;');
lines.push('');
// ── 1. admin_categories ────────────────────────────────────────────────────
lines.push('-- ── 1. Categories ────────────────────────────────────────────');
lines.push('INSERT IGNORE INTO admin_categories (value) VALUES');
const catValues = categories.map(c => ` ('${esc(c)}')`);
lines.push(catValues.join(',\n') + ';');
lines.push('');
// ── 2. admin_sub_categories ───────────────────────────────────────────────
// FK to admin_categories resolved via a UNION ALL derived table.
// (The VALUES ROW() syntax requires MariaDB ≥ 10.3.3; UNION ALL is universal.)
lines.push('-- ── 2. Sub-categories (FK resolved via sub-select) ──────────');
lines.push('INSERT IGNORE INTO admin_sub_categories (value, category_id)');
lines.push('SELECT vals.value, c.id');
lines.push('FROM (');
const scValues = subCats.map(
(sc, i) =>
` ${i === 0 ? 'SELECT' : 'UNION ALL SELECT'} '${esc(sc.subCat)}' AS value, '${esc(sc.category)}' AS cat_value`
);
lines.push(scValues.join('\n'));
lines.push(') AS vals');
lines.push('JOIN admin_categories c ON c.value = vals.cat_value;');
lines.push('');
// ── 3. admin_severities ──────────────────────────────────────────────────
lines.push('-- ── 3. Severities ───────────────────────────────────────────');
lines.push('INSERT IGNORE INTO admin_severities (value) VALUES');
const sevValues = severities.map(s => ` ('${esc(s)}')`);
lines.push(sevValues.join(',\n') + ';');
lines.push('');
// ── 4. admin_cl_records ──────────────────────────────────────────────────
lines.push('-- ── 4. Check-list records ───────────────────────────────────');
lines.push('INSERT IGNORE INTO admin_cl_records');
lines.push(' (sort_order, category, sub_category, severity, description_en, description_fr, description_nl)');
lines.push('VALUES');
const recValues = allRows.map(r =>
` (${r.sortOrder}, '${esc(r.category)}', '${esc(r.subCat)}', '${esc(r.severity)}', '${esc(r.descEn)}', '${esc(r.descFr)}', '${esc(r.descNl)}')`
);
lines.push(recValues.join(',\n') + ';');
lines.push('');
// ─── write output file ───────────────────────────────────────────────────────
const outPath = '/workspace/sql/cl-seed.sql';
fs.writeFileSync(outPath, lines.join('\n'), 'utf8');
console.log(`\nSQL written to ${outPath}`);
console.log(` ${categories.length} categories`);
console.log(` ${subCats.length} sub-categories`);
console.log(` ${severities.length} severities`);
console.log(` ${allRows.length} cl_records`);