const path = require('path'); const mysql = require('mysql2/promise'); require('dotenv').config({ path: path.join(__dirname, '..', 'backend', '.env') }); function toMs(value) { if (value === null || value === undefined || value === '') { return null; } const n = Number(value); if (!Number.isFinite(n) || n <= 0) { return null; } return n > 100000000000 ? n : n * 1000; } function toDate(value) { const ms = toMs(value); if (!ms) { return null; } const date = new Date(ms); return Number.isNaN(date.getTime()) ? null : date; } function toTaskKey(remark = '') { const tokens = String(remark || '') .toLowerCase() .replace(/[^\p{L}\p{N}\s]/gu, ' ') .split(/\s+/) .filter((token) => token.length >= 3) .slice(0, 6); return tokens.length > 0 ? tokens.join('_') : 'ukendt_opgave'; } async function main() { if (!process.env.DB_PASSWORD) { throw new Error('DB_PASSWORD is required'); } const conn = await mysql.createConnection({ host: process.env.DB_HOST || '127.0.0.1', port: parseInt(process.env.DB_PORT, 10) || 3306, user: process.env.DB_USER || 'tilbudgivern_service', password: process.env.DB_PASSWORD, database: process.env.DB_NAME || 'tilbudgivern' }); try { await conn.execute( ` CREATE TABLE IF NOT EXISTS ordrestyring_hours_normalized ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, case_number VARCHAR(64) NOT NULL, hour_id INT NOT NULL, is_case_linked TINYINT(1) NOT NULL DEFAULT 1, start_at DATETIME NULL, stop_at DATETIME NULL, duration_hours DECIMAL(10,2) NOT NULL DEFAULT 0, remark TEXT NULL, hour_type INT NULL, task_key VARCHAR(255) NULL, synced_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_hours_norm_case_hour (case_number, hour_id), INDEX idx_hours_norm_case (case_number), INDEX idx_hours_norm_case_linked (is_case_linked), INDEX idx_hours_norm_start (start_at) ) ` ); await conn.execute(` ALTER TABLE ordrestyring_hours_normalized ADD COLUMN IF NOT EXISTS is_case_linked TINYINT(1) NOT NULL DEFAULT 1 `); await conn.execute(` CREATE INDEX IF NOT EXISTS idx_hours_norm_case_linked ON ordrestyring_hours_normalized (is_case_linked) `); const [rows] = await conn.execute( ` SELECT h.id, h.new_case_number, h.start_time, h.stop_time, h.remark, h.hour_type FROM ordrestyring_local.hours h ` ); await conn.execute('TRUNCATE TABLE ordrestyring_hours_normalized'); const insertSql = ` INSERT INTO ordrestyring_hours_normalized ( case_number, hour_id, is_case_linked, start_at, stop_at, duration_hours, remark, hour_type, task_key ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `; const chunkSize = 1000; for (let i = 0; i < rows.length; i += chunkSize) { const chunk = rows.slice(i, i + chunkSize); for (const row of chunk) { const start = toDate(row.start_time); const stop = toDate(row.stop_time); let durationHours = 0; if (start && stop) { const diff = stop.getTime() - start.getTime(); if (diff > 0) { durationHours = Number((diff / 3600000).toFixed(2)); } } const rawCaseNumber = String(row.new_case_number || '').trim(); const isCaseLinked = rawCaseNumber.length > 0; const hourTypeValue = row.hour_type !== null && row.hour_type !== undefined ? Number(row.hour_type) : null; const normalizedCaseNumber = isCaseLinked ? rawCaseNumber : `__NON_CASE__:${hourTypeValue !== null ? hourTypeValue : 'unknown'}`; await conn.execute(insertSql, [ normalizedCaseNumber, Number(row.id), isCaseLinked ? 1 : 0, start, stop, durationHours, row.remark || null, hourTypeValue, toTaskKey(row.remark) ]); } } const [[stats]] = await conn.execute( ` SELECT COUNT(*) AS rows_inserted, COUNT(DISTINCT case_number) AS distinct_cases, SUM(CASE WHEN is_case_linked = 1 THEN 1 ELSE 0 END) AS rows_case_linked, SUM(CASE WHEN is_case_linked = 0 THEN 1 ELSE 0 END) AS rows_non_case, SUM(CASE WHEN duration_hours > 0 THEN 1 ELSE 0 END) AS rows_with_duration, SUM(CASE WHEN remark IS NOT NULL AND TRIM(remark) <> '' THEN 1 ELSE 0 END) AS rows_with_remark FROM ordrestyring_hours_normalized ` ); console.log(JSON.stringify({ table: 'ordrestyring_hours_normalized', rowsInserted: Number(stats.rows_inserted || 0), distinctCases: Number(stats.distinct_cases || 0), rowsCaseLinked: Number(stats.rows_case_linked || 0), rowsNonCase: Number(stats.rows_non_case || 0), rowsWithDuration: Number(stats.rows_with_duration || 0), rowsWithRemark: Number(stats.rows_with_remark || 0) }, null, 2)); } finally { await conn.end(); } } main().catch((error) => { console.error(error); process.exit(1); });