131 lines
5.0 KiB
JavaScript
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);
|
|
});
|