310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
/**
|
|
* TILBUDSGENERERING BASERET PÅ REELLE TIDSDATA
|
|
* Intelligent system til beregning af realistiske tilbud
|
|
*
|
|
* Baseret på analyse af 1093 reelle projekter fra Ordrestyring database
|
|
* Erstatter teoretiske PDF-estimater med faktiske gennemsnitsdata
|
|
*/
|
|
|
|
const RealTimeCarpenterTasks = require('./real-time-carpenter-tasks.js');
|
|
|
|
class IntelligentQuoteGenerator {
|
|
constructor() {
|
|
this.carpenterDB = new RealTimeCarpenterTasks();
|
|
this.hourlyRates = {
|
|
standard: 450, // kr/time for standard håndværk
|
|
specialist: 550, // kr/time for specialiseret arbejde
|
|
complex: 650, // kr/time for komplekst/risikofyldt arbejde
|
|
material_markup: 1.25 // 25% tillæg på materialer
|
|
};
|
|
|
|
// Kompleksitetsfaktorer baseret på reelle data analyse
|
|
this.complexityFactors = {
|
|
simple: 0.8, // Små, enkle opgaver
|
|
standard: 1.0, // Standard kompleksitet
|
|
complex: 1.3, // Komplekse forhold (højder, adgang, etc.)
|
|
very_complex: 1.6 // Meget komplekse (tilbygninger, store tag)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Genererer tilbud for specifik opgave type
|
|
* @param {string} taskType - Type opgave (tag_small, kitchen, etc.)
|
|
* @param {number} size - Størrelse (m², løbende meter, etc.)
|
|
* @param {string} complexity - simple|standard|complex|very_complex
|
|
* @param {Object} additionalFactors - Ekstra faktorer
|
|
* @returns {Object} Komplet tilbud
|
|
*/
|
|
generateQuote(taskType, size, complexity = 'standard', additionalFactors = {}) {
|
|
const taskCategory = this.carpenterDB.taskTimeCategories[taskType];
|
|
if (!taskCategory) {
|
|
throw new Error(`Ukendt opgave type: ${taskType}`);
|
|
}
|
|
|
|
const baseHours = taskCategory.avgHours;
|
|
const complexityFactor = this.complexityFactors[complexity] || 1.0;
|
|
|
|
// Juster timer baseret på størrelse og kompleksitet
|
|
const adjustedHours = this.calculateAdjustedHours(baseHours, size, complexityFactor, additionalFactors);
|
|
|
|
// Vælg timelløn baseret på opgave type
|
|
const hourlyRate = this.selectHourlyRate(taskType, complexity);
|
|
|
|
// Beregn arbejdsløn
|
|
const laborCost = adjustedHours * hourlyRate;
|
|
|
|
// Estimer materialeomkostninger
|
|
const materialCost = this.estimateMaterialCost(taskType, size, additionalFactors);
|
|
|
|
// Beregn moms og total
|
|
const subtotal = laborCost + materialCost;
|
|
const vat = subtotal * 0.25; // 25% moms
|
|
const total = subtotal + vat;
|
|
|
|
return {
|
|
taskDetails: {
|
|
type: taskType,
|
|
description: taskCategory.description,
|
|
size: size,
|
|
complexity: complexity,
|
|
projectsBasedOn: taskCategory.projects
|
|
},
|
|
timeEstimate: {
|
|
baseHours: baseHours,
|
|
adjustedHours: adjustedHours.toFixed(1),
|
|
complexityFactor: complexityFactor,
|
|
workingDays: Math.ceil(adjustedHours / 7.4) // 7.4 timer pr dag
|
|
},
|
|
costs: {
|
|
labor: {
|
|
hours: adjustedHours.toFixed(1),
|
|
hourlyRate: hourlyRate,
|
|
amount: laborCost.toFixed(0)
|
|
},
|
|
materials: {
|
|
estimated: materialCost.toFixed(0),
|
|
markup: `${((this.hourlyRates.material_markup - 1) * 100)}%`
|
|
},
|
|
subtotal: subtotal.toFixed(0),
|
|
vat: vat.toFixed(0),
|
|
total: total.toFixed(0)
|
|
},
|
|
dataSource: 'Baseret på ' + taskCategory.projects + ' reelle projekter fra database',
|
|
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] // 30 dage
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Beregner justerede timer baseret på forskellige faktorer
|
|
*/
|
|
calculateAdjustedHours(baseHours, size, complexityFactor, additionalFactors) {
|
|
let adjustedHours = baseHours * complexityFactor;
|
|
|
|
// Størrelses justeringer - større projekter har skalafordele
|
|
if (size) {
|
|
if (size > 100) {
|
|
adjustedHours *= 0.95; // 5% rabat for store projekter
|
|
} else if (size < 20) {
|
|
adjustedHours *= 1.1; // 10% tillæg for meget små projekter
|
|
}
|
|
}
|
|
|
|
// Adgangs vanskeligheder
|
|
if (additionalFactors.difficultAccess) {
|
|
adjustedHours *= 1.2;
|
|
}
|
|
|
|
// Højtarbejde
|
|
if (additionalFactors.heightWork) {
|
|
adjustedHours *= 1.15;
|
|
}
|
|
|
|
// Gammelt byggeri (kan have overraskelser)
|
|
if (additionalFactors.oldBuilding && additionalFactors.oldBuilding > 1970) {
|
|
adjustedHours *= 1.1;
|
|
} else if (additionalFactors.oldBuilding && additionalFactors.oldBuilding < 1950) {
|
|
adjustedHours *= 1.25;
|
|
}
|
|
|
|
return adjustedHours;
|
|
}
|
|
|
|
/**
|
|
* Vælger passende timeløn baseret på opgave kompleksitet
|
|
*/
|
|
selectHourlyRate(taskType, complexity) {
|
|
// Tag arbejde og tilbygninger kræver specialist kompetencer
|
|
if (taskType.includes('tag_') || taskType === 'extension') {
|
|
return complexity === 'very_complex' ?
|
|
this.hourlyRates.complex : this.hourlyRates.specialist;
|
|
}
|
|
|
|
// Vinduer/døre kræver præcision
|
|
if (taskType === 'windows_doors') {
|
|
return this.hourlyRates.specialist;
|
|
}
|
|
|
|
// Ombygning kan være uforudsigelig
|
|
if (taskType === 'renovation') {
|
|
return complexity === 'simple' ?
|
|
this.hourlyRates.standard : this.hourlyRates.specialist;
|
|
}
|
|
|
|
// Standard opgaver
|
|
return this.hourlyRates.standard;
|
|
}
|
|
|
|
/**
|
|
* Estimerer materialeomkostninger
|
|
*/
|
|
estimateMaterialCost(taskType, size, additionalFactors) {
|
|
const materialRates = {
|
|
tag_small: 300, // kr/m²
|
|
tag_medium: 400, // kr/m²
|
|
tag_large: 500, // kr/m²
|
|
tag_xlarge: 600, // kr/m²
|
|
kitchen: 8000, // kr/løbende meter
|
|
bathroom: 1200, // kr/m²
|
|
flooring: 400, // kr/m²
|
|
windows_doors: 3000, // kr/stk
|
|
outdoor: 600, // kr/m²
|
|
renovation: 300, // kr/m²
|
|
extension: 2500 // kr/m²
|
|
};
|
|
|
|
const baseMaterialCost = (materialRates[taskType] || 300) * (size || 1);
|
|
|
|
// Tillæg for premium materialer
|
|
if (additionalFactors.premiumMaterials) {
|
|
return baseMaterialCost * 1.5 * this.hourlyRates.material_markup;
|
|
}
|
|
|
|
return baseMaterialCost * this.hourlyRates.material_markup;
|
|
}
|
|
|
|
/**
|
|
* Genererer komplet tilbud for flere opgaver
|
|
*/
|
|
generateMultiTaskQuote(tasks) {
|
|
const quotes = tasks.map(task =>
|
|
this.generateQuote(task.type, task.size, task.complexity, task.additionalFactors)
|
|
);
|
|
|
|
const totalHours = quotes.reduce((sum, q) => sum + parseFloat(q.timeEstimate.adjustedHours), 0);
|
|
const totalCost = quotes.reduce((sum, q) => sum + parseFloat(q.costs.total), 0);
|
|
const totalDays = Math.ceil(totalHours / 7.4);
|
|
|
|
return {
|
|
tasks: quotes,
|
|
summary: {
|
|
totalHours: totalHours.toFixed(1),
|
|
totalDays: totalDays,
|
|
totalCost: totalCost.toFixed(0),
|
|
averageHourlyRate: (totalCost / totalHours).toFixed(0)
|
|
},
|
|
projectSchedule: this.generateProjectSchedule(quotes)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Genererer projekt tidsplan
|
|
*/
|
|
generateProjectSchedule(quotes) {
|
|
let currentDay = 1;
|
|
const schedule = [];
|
|
|
|
quotes.forEach((quote, index) => {
|
|
const taskDays = Math.ceil(parseFloat(quote.timeEstimate.adjustedHours) / 7.4);
|
|
schedule.push({
|
|
taskNumber: index + 1,
|
|
description: quote.taskDetails.description,
|
|
startDay: currentDay,
|
|
endDay: currentDay + taskDays - 1,
|
|
duration: taskDays + ' dage'
|
|
});
|
|
currentDay += taskDays;
|
|
});
|
|
|
|
return schedule;
|
|
}
|
|
|
|
/**
|
|
* Eksempel tilbud for demonstration
|
|
*/
|
|
generateExampleQuotes() {
|
|
const examples = [
|
|
{
|
|
name: 'Tag renovering - parcelhus',
|
|
quote: this.generateQuote('tag_medium', 120, 'standard', {
|
|
oldBuilding: 1985,
|
|
heightWork: true
|
|
})
|
|
},
|
|
{
|
|
name: 'Køkken installation - rækkehus',
|
|
quote: this.generateQuote('kitchen', 4, 'standard', {
|
|
premiumMaterials: false
|
|
})
|
|
},
|
|
{
|
|
name: 'Terrasse - kompleks form',
|
|
quote: this.generateQuote('outdoor', 35, 'complex', {
|
|
difficultAccess: true,
|
|
premiumMaterials: true
|
|
})
|
|
},
|
|
{
|
|
name: 'Komplet tilbygning',
|
|
quote: this.generateQuote('extension', 25, 'very_complex', {
|
|
oldBuilding: 1960
|
|
})
|
|
}
|
|
];
|
|
|
|
return examples;
|
|
}
|
|
}
|
|
|
|
// Test og demonstration
|
|
if (require.main === module) {
|
|
console.log('💼 INTELLIGENT TILBUDSGENERATOR');
|
|
console.log('='.repeat(60));
|
|
console.log('Baseret på reelle data fra 1093 projekter\n');
|
|
|
|
const quoteGen = new IntelligentQuoteGenerator();
|
|
|
|
// Generer eksempel tilbud
|
|
const examples = quoteGen.generateExampleQuotes();
|
|
|
|
examples.forEach((example, i) => {
|
|
console.log(`\n📋 TILBUD ${i+1}: ${example.name.toUpperCase()}`);
|
|
console.log('-'.repeat(50));
|
|
|
|
const q = example.quote;
|
|
console.log(`Opgave: ${q.taskDetails.description}`);
|
|
console.log(`Størrelse: ${q.taskDetails.size} (${q.taskDetails.complexity})`);
|
|
console.log(`Estimeret tid: ${q.timeEstimate.adjustedHours} timer (${q.timeEstimate.workingDays} dage)`);
|
|
console.log(`Arbejdsløn: ${parseInt(q.costs.labor.amount).toLocaleString()} kr`);
|
|
console.log(`Materialer: ${parseInt(q.costs.materials.estimated).toLocaleString()} kr`);
|
|
console.log(`TOTAL inkl. moms: ${parseInt(q.costs.total).toLocaleString()} kr`);
|
|
console.log(`Data baseret på: ${q.dataSource}`);
|
|
});
|
|
|
|
// Sammenligning med PDF estimater
|
|
console.log('\n\n🔍 SAMMENLIGNING MED PDF-ESTIMATER:');
|
|
console.log('='.repeat(60));
|
|
|
|
const tagQuote = quoteGen.generateQuote('tag_medium', 100, 'standard');
|
|
const pdfTagEstimate = 12.5 * 450; // PDF timer * standard timeløn
|
|
|
|
console.log(`Tag projekt (100m²):`);
|
|
console.log(` PDF estimat: ${pdfTagEstimate.toLocaleString()} kr (12.5 timer)`);
|
|
console.log(` Reelt estimat: ${parseInt(tagQuote.costs.labor.amount).toLocaleString()} kr (${tagQuote.timeEstimate.adjustedHours} timer)`);
|
|
console.log(` Forskel: ${(parseInt(tagQuote.costs.labor.amount) - pdfTagEstimate).toLocaleString()} kr mere`);
|
|
console.log(` Faktor: ${(parseInt(tagQuote.costs.labor.amount) / pdfTagEstimate).toFixed(1)}x længere`);
|
|
|
|
console.log('\n✅ Tilbudsgenerator klar til brug!');
|
|
console.log('💡 Brug generateQuote() metoden til specifikke tilbud');
|
|
}
|
|
|
|
module.exports = IntelligentQuoteGenerator; |