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