Files
tilbudgivern/scripts/mine-ordrestyring-keyword-packages.js
2026-07-05 08:10:59 +00:00

165 lines
5.6 KiB
JavaScript

const fs = require('fs/promises');
const path = require('path');
const mysql = require('mysql2/promise');
const dotenv = require('dotenv');
const { ORDRESTYRING_PACKAGE_KEYWORD_RULES } = require('../backend/src/constants/ordrestyringPackageKeywordRules');
dotenv.config({ path: path.join(__dirname, '..', 'backend/.env') });
const STOPWORDS = new Set([
'og', 'det', 'der', 'for', 'til', 'med', 'som', 'på', 'af', 'i', 'at', 'er', 'en', 'et', 'de',
'den', 'fra', 'skal', 'blev', 'ikke', 'ved', 'eller', 'om', 'har', 'have', 'kan', 'ud', 'ind',
'tag', 'arbejde', 'opgave', 'sag', 'kunde', 'projekt'
]);
function tokenize(value) {
return (value || '')
.toLowerCase()
.replace(/[\n\r\t]+/g, ' ')
.split(/[\s,.;:/()_\-]+/)
.map(token => token.trim())
.filter(token => token.length >= 3 && !STOPWORDS.has(token));
}
function pickPackageByKeywords(text) {
for (const rule of ORDRESTYRING_PACKAGE_KEYWORD_RULES) {
if (rule.keywords.some(keyword => text.includes(keyword.toLowerCase()))) {
return rule.packageId;
}
}
return text.includes('tag') ? 'komplet_tagrenovering' : null;
}
function createBucket() {
return {
caseCount: 0,
totalHours: 0,
totalMaterialLines: 0,
totalMaterialValue: 0,
sampleCases: [],
tokenCounts: new Map()
};
}
async function main() {
if (!process.env.DB_PASSWORD) {
throw new Error('DB_PASSWORD mangler i miljøet');
}
const connection = await mysql.createConnection({
host: process.env.DB_HOST || '127.0.0.1',
port: parseInt(process.env.DB_PORT || '3306', 10),
user: process.env.DB_USER || 'tilbudgivern_service',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'tilbudgivern'
});
try {
const [cases] = await connection.execute(
`
SELECT
case_number,
description,
remarks,
search_text,
inferred_package_id,
inferred_roof_type,
total_hours,
material_lines_count,
material_total,
last_activity_at_ordrestyring
FROM ordrestyring_case_features
WHERE case_number IS NOT NULL
AND case_number <> ''
AND (
COALESCE(NULLIF(TRIM(inferred_package_id), ''), '') <> ''
OR COALESCE(NULLIF(TRIM(inferred_roof_type), ''), '') <> ''
OR LOWER(COALESCE(search_text, '')) LIKE '%tag%'
OR LOWER(COALESCE(search_text, '')) LIKE '%velux%'
OR LOWER(COALESCE(search_text, '')) LIKE '%tegl%'
OR LOWER(COALESCE(search_text, '')) LIKE '%skorsten%'
OR LOWER(COALESCE(search_text, '')) LIKE '%eternit%'
OR LOWER(COALESCE(search_text, '')) LIKE '%tagpap%'
)
`
);
const buckets = new Map();
for (const row of cases) {
const text = `${row.search_text || ''} ${row.description || ''} ${row.remarks || ''}`.toLowerCase();
const packageId = (row.inferred_package_id || '').trim() || pickPackageByKeywords(text);
if (!packageId) {
continue;
}
if (!buckets.has(packageId)) {
buckets.set(packageId, createBucket());
}
const bucket = buckets.get(packageId);
bucket.caseCount += 1;
bucket.totalHours += parseFloat(row.total_hours) || 0;
bucket.totalMaterialLines += parseInt(row.material_lines_count, 10) || 0;
bucket.totalMaterialValue += parseFloat(row.material_total) || 0;
if (bucket.sampleCases.length < 8) {
bucket.sampleCases.push({
caseNumber: row.case_number,
description: (row.description || '').slice(0, 280),
remarks: (row.remarks || '').slice(0, 180),
totalHours: parseFloat(row.total_hours) || 0,
materialLines: parseInt(row.material_lines_count, 10) || 0,
materialTotal: parseFloat(row.material_total) || 0,
lastActivityAt: row.last_activity_at_ordrestyring || null
});
}
tokenize(text).forEach(token => {
bucket.tokenCounts.set(token, (bucket.tokenCounts.get(token) || 0) + 1);
});
}
const packageSuggestions = [...buckets.entries()]
.map(([packageId, bucket]) => {
const topKeywords = [...bucket.tokenCounts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([token, count]) => ({ token, count }));
return {
packageId,
caseCount: bucket.caseCount,
avgHours: bucket.caseCount > 0 ? Number((bucket.totalHours / bucket.caseCount).toFixed(2)) : 0,
avgMaterialLines: bucket.caseCount > 0 ? Number((bucket.totalMaterialLines / bucket.caseCount).toFixed(2)) : 0,
avgMaterialValue: bucket.caseCount > 0 ? Number((bucket.totalMaterialValue / bucket.caseCount).toFixed(2)) : 0,
topKeywords,
sampleCases: bucket.sampleCases
};
})
.sort((a, b) => b.caseCount - a.caseCount);
const output = {
generatedAt: new Date().toISOString(),
totalTagCasesScanned: cases.length,
packageSuggestions
};
const outDir = path.join(__dirname, '..', 'docs', 'status-reports');
const outPath = path.join(outDir, 'ORDRESTYRING_KEYWORD_ENGINE_latest.json');
await fs.mkdir(outDir, { recursive: true });
await fs.writeFile(outPath, JSON.stringify(output, null, 2), 'utf8');
console.log(`Analyseret ${cases.length} tag-relaterede Ordrestyring cases`);
console.log(`Pakke-forslag: ${packageSuggestions.length}`);
console.log(`Rapport: ${outPath}`);
} finally {
await connection.end();
}
}
main().catch(error => {
console.error('Keyword-engine fejl:', error.message);
process.exit(1);
});