Files
tilbudgivern/backend/scripts/backfill-smart-package-defaults.js
2026-08-10 12:38:52 +02:00

131 lines
5.0 KiB
JavaScript

require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
const mysql = require('mysql2/promise');
const { distributeTaskTime } = require('../src/services/smartPackageDefaultsService');
const {
findBestMaterialMatch,
loadMaterialCatalog
} = require('../src/services/smartPackageMaterialMatchService');
const parseQuantity = value => {
const match = String(value || '').replace(',', '.').match(/-?\d+(?:\.\d+)?/);
return match ? Math.max(0, parseFloat(match[0]) || 0) : 0;
};
const main = async () => {
const apply = process.argv.includes('--apply');
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
const [taskRows] = await connection.execute(`
SELECT mp.id AS package_id, mp.name AS package_name, mp.hours_unit,
COALESCE(NULLIF(mp.time_per_unit, 0), NULLIF(mp.time_per_sqm, 0), mp.estimated_hours, 0) AS total_time,
t.id, t.name, t.description, t.hours, t.time_per_unit
FROM material_packages mp
JOIN smart_package_tasks t ON t.package_id = mp.id
WHERE mp.is_active = 1
ORDER BY mp.id, t.task_order, t.id
`);
const groupedTasks = new Map();
for (const row of taskRows) {
const group = groupedTasks.get(row.package_id) || [];
group.push(row);
groupedTasks.set(row.package_id, group);
}
const taskUpdates = [];
for (const rows of groupedTasks.values()) {
const totalTime = parseFloat(rows[0].total_time || 0);
const hasTime = rows.some(row => (parseFloat(row.hours) || 0) > 0 || (parseFloat(row.time_per_unit) || 0) > 0);
if (totalTime <= 0 || hasTime) continue;
const distributed = distributeTaskTime(rows, totalTime, rows[0].hours_unit);
distributed.forEach((task, index) => taskUpdates.push({
id: rows[index].id,
packageId: rows[index].package_id,
packageName: rows[index].package_name,
taskName: rows[index].name,
hours: task.hours,
timePerUnit: task.timePerUnit,
timeUnit: task.timeUnit
}));
}
const catalog = await loadMaterialCatalog(connection);
const [materialRows] = await connection.execute(`
SELECT pm.id, pm.package_id, pm.material_name AS name, pm.unit, pm.item_code AS itemCode,
pm.raw_line AS rawLine, pm.quantity_text AS quantityText, pm.quantity
FROM package_materials pm
JOIN material_packages mp ON mp.id = pm.package_id AND mp.is_active = 1
WHERE pm.material_id IS NULL
ORDER BY pm.id
`);
const materialUpdates = materialRows.map(row => ({ row, match: findBestMaterialMatch(row, catalog) }));
const matchedMaterials = materialUpdates.filter(update => update.match.material);
if (apply) {
await connection.beginTransaction();
try {
for (const task of taskUpdates) {
await connection.execute(`
UPDATE smart_package_tasks
SET hours = ?, time_per_unit = ?, time_unit = ?
WHERE id = ?
`, [task.hours, task.timePerUnit, task.timeUnit, task.id]);
}
for (const { row, match } of matchedMaterials) {
const quantity = parseFloat(row.quantity || 0) || parseQuantity(row.quantityText);
const unitPrice = parseFloat(match.material.price || 0);
await connection.execute(`
UPDATE package_materials
SET material_id = ?, material_match_status = ?, material_match_score = ?,
quantity = ?, unit = ?, unit_price = ?, total_price = ?
WHERE id = ?
`, [
match.material.id, match.status, match.score, quantity, match.material.unit || row.unit,
unitPrice, quantity * unitPrice, row.id
]);
}
for (const { row, match } of materialUpdates.filter(update => !update.match.material)) {
await connection.execute(`
UPDATE package_materials
SET material_match_status = ?, material_match_score = ?
WHERE id = ?
`, [match.status, match.score, row.id]);
}
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
}
}
console.log(JSON.stringify({
mode: apply ? 'apply' : 'dry-run',
taskUpdates: taskUpdates.length,
taskPackages: new Set(taskUpdates.map(row => row.packageId)).size,
packageMaterialRows: materialRows.length,
matchedMaterials: matchedMaterials.length,
ambiguousMaterials: materialUpdates.filter(update => update.match.status === 'ambiguous').length,
unmatchedMaterials: materialUpdates.filter(update => update.match.status === 'unmatched').length,
taskSample: taskUpdates.slice(0, 12),
materialSample: matchedMaterials.slice(0, 12).map(({ row, match }) => ({
packageMaterialId: row.id,
sourceName: row.name,
materialId: match.material.id,
matchedName: match.material.name,
price: match.material.price,
score: Number(match.score.toFixed(3))
}))
}, null, 2));
await connection.end();
};
main().catch(error => {
console.error(error);
process.exit(1);
});