165 lines
5.6 KiB
JavaScript
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);
|
|
});
|