Files
tilbudgivern/scripts/normalize-hours.js

178 lines
5.2 KiB
JavaScript

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);
});