feat: Implementere erfaringsbaserede SmartPackages med varenumre og geometri-beregning

- Oprettet create-packages-simple.js til at generere SmartPackages med detaljerede materialelister og installationstider.
- Oprettet create-packages-v3.js som en minimal version til oprettelse af basis erfaringspakker.
- Tilføjet test-markup-sliders.js for at teste markup sliders for materialer og final review.
- Implementeret test-package-calculation.js til at teste geometri-beregninger for SmartPackages.
- Oprettet test-stern-til-kip-formula.js for at validere brugen af stern_til_kip variabel i formler.
- Tilføjet test-stern-to-ridge-height.js for at sikre korrekt gemning af højde stern til kip i databasen.
This commit is contained in:
alexpolo1
2025-10-25 04:52:26 +00:00
parent 9f7ff8456b
commit dc4a470a58
14 changed files with 2113 additions and 60 deletions

275
MARKUP_SLIDER_SYSTEM.md Normal file
View File

@@ -0,0 +1,275 @@
# Markup Slider System for SmartPackages
## 📊 Oversigt
SmartPackage systemet understøtter nu **to niveauer af markup** som kan justeres individuelt for hvert tilbud:
### 1. Material Markup Slider 💰
- **Standard:** 20%
- **Justerbar:** 0% - 50%
- **Placering:** Ved tilføjelse af SmartPackage til tilbud
- **Anvendelse:** Påføres alle materialer fra pakken
- **Gemt i:** `quotes.material_markup_percent`
### 2. Final Review Markup Slider 📈
- **Standard:** 2%
- **Justerbar:** 0% - 10%
- **Placering:** I final review trinnet
- **Anvendelse:** Påføres på subtotal (materialer + arbejde)
- **Gemt i:** `quotes.final_review_markup_percent`
---
## 🔧 Database Struktur
### Nye felter i `quotes` tabel:
```sql
ALTER TABLE quotes
ADD COLUMN material_markup_percent DECIMAL(5,2) DEFAULT 20.00
COMMENT 'Material markup procent (standard 20%)',
ADD COLUMN final_review_markup_percent DECIMAL(5,2) DEFAULT 2.00
COMMENT 'Final review markup procent (standard 2%)';
```
---
## 🔌 API Endpoint
### POST `/api/smart-packages/:id/calculate-with-geometry`
**Request body:**
```json
{
"projectId": 123, // Optional: Hent geometri og markup fra database
"geometry": { // Optional: Brug custom geometri
"length_main": 15,
"width_main": 10,
"total_area": 85,
"nedløb_antal": 4
},
"materialMarkupPercent": 20, // Optional: Override material markup
"finalReviewMarkupPercent": 2 // Optional: Override final review markup
}
```
**Response:**
```json
{
"success": true,
"package": {
"id": 97,
"name": "Tagrende med Konsoljern"
},
"materials": [
{
"varenr": "077424",
"name": "Tagrende 3m",
"quantity": 5,
"base_price": 150.00, // Indkøbspris
"price": 180.00, // Med markup (150 * 1.20)
"markup_percent": 20
}
],
"markup": {
"material_markup_percent": 20,
"final_review_markup_percent": 2,
"final_review_amount": 87.00
},
"totals": {
"material_cost": 900.00, // Med material markup
"labor_cost": 4350.00,
"subtotal": 5250.00,
"final_review_markup": 87.00, // 2% af subtotal
"total_cost": 5337.00 // Alt inkluderet
}
}
```
---
## 💡 Beregningseksempler
### Eksempel 1: Standard (20% + 2%)
```
Materialer (indkøb): 750 kr
+ Material markup (20%): +150 kr
= Materialer til kunde: 900 kr
Arbejde: 4350 kr
─────────────────────────
Subtotal: 5250 kr
+ Final review markup (2%): +105 kr
═════════════════════════
TOTAL: 5355 kr
```
### Eksempel 2: Lav margin (10% + 1%)
```
Materialer (indkøb): 750 kr
+ Material markup (10%): +75 kr
= Materialer til kunde: 825 kr
Arbejde: 4350 kr
─────────────────────────
Subtotal: 5175 kr
+ Final review markup (1%): +51.75 kr
═════════════════════════
TOTAL: 5226.75 kr
```
### Eksempel 3: Høj margin (30% + 5%)
```
Materialer (indkøb): 750 kr
+ Material markup (30%): +225 kr
= Materialer til kunde: 975 kr
Arbejde: 4350 kr
─────────────────────────
Subtotal: 5325 kr
+ Final review markup (5%): +266.25 kr
═════════════════════════
TOTAL: 5591.25 kr
```
---
## 🎨 UI Implementation Guide
### 1. Material Markup Slider
**Placering:** Når bruger tilføjer SmartPackage til tilbud
```jsx
<div className="markup-control">
<label>Material Markup</label>
<input
type="range"
min="0"
max="50"
value={materialMarkup}
onChange={(e) => setMaterialMarkup(e.target.value)}
/>
<span>{materialMarkup}%</span>
<p className="help-text">
Avance materialer (standard: 20%)
</p>
</div>
```
**Når skal den vises:**
- Ved tilføjelse af SmartPackage til tilbud
- I tilbudsredigering
- Som standard værdi ved oprettelse af nyt tilbud
### 2. Final Review Markup Slider
**Placering:** I final review trinnet før tilbuddet sendes
```jsx
<div className="final-markup-control">
<label>Final Review Markup</label>
<input
type="range"
min="0"
max="10"
step="0.1"
value={finalReviewMarkup}
onChange={(e) => setFinalReviewMarkup(e.target.value)}
/>
<span>{finalReviewMarkup}%</span>
<p className="help-text">
Ekstra margin total pris (standard: 2%)
</p>
<div className="markup-preview">
<p>Subtotal: {formatCurrency(subtotal)}</p>
<p>Final markup (+{finalReviewMarkup}%): {formatCurrency(finalMarkupAmount)}</p>
<p className="total">Total: {formatCurrency(total)}</p>
</div>
</div>
```
**Når skal den vises:**
- I final review trinnet
- Kun hvis brugeren vil justere den
- Kan være skjult bag "Avancerede indstillinger"
---
## 📝 Workflow
### Trin 1: Opret tilbud
```javascript
// Når tilbud oprettes, sæt default markup værdier
const newQuote = {
customer_name: "Kunde Navn",
material_markup_percent: 20.00, // Default
final_review_markup_percent: 2.00 // Default
};
```
### Trin 2: Tilføj SmartPackage
```javascript
// Bruger kan justere material markup
const addPackageResponse = await axios.post(
`/api/smart-packages/97/calculate-with-geometry`,
{
projectId: quoteId,
materialMarkupPercent: 25 // Bruger satte 25%
}
);
// Gem i quote
await axios.put(`/api/quotes/${quoteId}`, {
material_markup_percent: 25
});
```
### Trin 3: Final Review
```javascript
// Bruger kan justere final review markup
const updateFinalMarkup = async (newMarkup) => {
await axios.put(`/api/quotes/${quoteId}`, {
final_review_markup_percent: newMarkup
});
// Genberegn totals
await recalculateTotals();
};
```
---
## ✅ Test
Kør test script:
```bash
node test-markup-sliders.js
```
Dette tester 4 scenarier:
1. Standard (20% + 2%)
2. Lav margin (10% + 1%)
3. Høj margin (30% + 5%)
4. Ingen final markup (20% + 0%)
---
## 🔄 Næste Skridt
1. **Frontend Implementation**
- Tilføj material markup slider ved package tilføjelse
- Tilføj final review markup slider i review trin
- Vis markup breakdown tydeligt i UI
2. **Quote Management**
- Gem markup værdier når quote opdateres
- Hent markup værdier når quote loades
- Vis aktuel markup i quote oversigt
3. **Material Import**
- Import Bygma materialer med korrekte indkøbspriser
- Så markup kan beregnes på reelle priser
4. **Rapportering**
- Vis markup breakdown i PDF tilbud
- Vis profit margin i analytics
- Track gennemsnitlig markup per kunde/projekt

View File

@@ -175,7 +175,8 @@ router.post('/projects/:id/geometry', async (req, res) => {
totalArea: requestData.totalArea || (requestData.roofWidth * requestData.roofLength),
roofPitch: requestData.roofPitch,
roofHeight: requestData.ridgeHeight || requestData.roofHeight,
wallHeight: requestData.wallHeight,
sternToRidgeHeight: requestData.wallHeight || requestData.sternToRidgeHeight, // Højde stern til kip
wallHeight: requestData.wallHeight, // Legacy support
lengthMain: requestData.roofLength || requestData.lengthMain,
widthMain: requestData.roofWidth || requestData.widthMain,
hasDormers: requestData.hasDormers || false,

View File

@@ -127,6 +127,125 @@ router.get('/categories', async (req, res) => {
}
});
// Beregn pakke med geometri - NYT ENDPOINT
router.post('/:id/calculate-with-geometry', async (req, res) => {
try {
const { id } = req.params;
const { projectId, geometry, materialMarkupPercent, finalReviewMarkupPercent } = req.body;
// Brug angivet markup eller hent fra quote/default
// Brug ?? (nullish coalescing) så 0 ikke bliver behandlet som falsy
let materialMarkup = materialMarkupPercent ?? 20; // Standard 20%
let finalReviewMarkup = finalReviewMarkupPercent ?? 2; // Standard 2%
// Hvis projectId er angivet, hent geometri og markup fra database
let geometryData = geometry;
if (projectId && !geometry) {
const db = global.databaseService;
// Hent geometri
const [geoRows] = await db.pool.execute(
'SELECT * FROM roof_geometry WHERE project_id = ?',
[projectId]
);
if (geoRows.length > 0) {
geometryData = geoRows[0];
} else {
return res.status(404).json({
success: false,
error: 'Geometri ikke fundet for projekt'
});
}
// Hent markup fra quote hvis ikke angivet
if (materialMarkupPercent === undefined || finalReviewMarkupPercent === undefined) {
const [quoteRows] = await db.pool.execute(
'SELECT material_markup_percent, final_review_markup_percent FROM quotes WHERE id = ?',
[projectId]
);
if (quoteRows.length > 0) {
materialMarkup = materialMarkupPercent ?? quoteRows[0].material_markup_percent ?? 20;
finalReviewMarkup = finalReviewMarkupPercent ?? quoteRows[0].final_review_markup_percent ?? 2;
}
}
}
if (!geometryData) {
return res.status(400).json({
success: false,
error: 'Geometri data er påkrævet (geometry eller projectId)'
});
}
// Beregn materialer og timer med formler (med material markup)
const result = await getService().calculatePackageMaterialsWithGeometry(
id,
geometryData,
materialMarkup
);
// Beregn arbejdstimer fra beskrivelse
const laborHours = await getService().calculateLaborHours(
result.package.description,
geometryData
);
// Beregn totaler UDEN final review markup først
const materialCostBeforeFinal = result.materials.reduce((sum, m) => sum + (m.quantity * m.price), 0);
const laborCost = laborHours * (result.package.hourly_rate || 580);
const subtotal = materialCostBeforeFinal + laborCost;
// Påfør final review markup på subtotal
const finalReviewAmount = subtotal * (finalReviewMarkup / 100);
const totalCost = subtotal + finalReviewAmount;
res.json({
success: true,
package: {
id: result.package.id,
name: result.package.name,
description: result.package.description,
category: result.package.category
},
geometry: geometryData,
materials: result.materials,
labor: {
hours: laborHours,
hourly_rate: result.package.hourly_rate || 580,
cost: laborCost
},
markup: {
material_markup_percent: materialMarkup,
final_review_markup_percent: finalReviewMarkup,
final_review_amount: finalReviewAmount
},
totals: {
material_cost: materialCostBeforeFinal,
labor_cost: laborCost,
subtotal: subtotal,
final_review_markup: finalReviewAmount,
total_cost: totalCost
},
calculation_info: {
geometry_used: {
facadelængde: geometryData.length_main,
husbredde: geometryData.width_main,
tagflade_m2: geometryData.total_area
},
markup_info: `Materialer inkl. ${materialMarkup}% avance. Total inkl. ${finalReviewMarkup}% final review markup.`
}
});
} catch (error) {
console.error('Error calculating package with geometry:', error);
res.status(500).json({
success: false,
error: 'Fejl ved beregning med geometri: ' + error.message
});
}
});
// Hent en specifik smart pakke med detaljer
router.get('/:id', async (req, res) => {
try {

View File

@@ -295,6 +295,15 @@ class SmartPackageManagementService {
SELECT * FROM package_materials WHERE package_id = ? ORDER BY material_category, material_name
`, [packageId]);
// Map materialer til det format frontend forventer
const materials = materialsRows.map(m => ({
...m,
quantity: parseFloat(m.quantity), // Fjern unødvendige decimaler
price: parseFloat(m.unit_price || 0),
custom_price: null,
material_description: m.notes || ''
}));
// Hent installation manuals hvis der er nogen koblet
const [manualsRows] = await this.db.pool.execute(`
SELECT im.*
@@ -314,7 +323,7 @@ class SmartPackageManagementService {
return {
...packageData,
tasks,
materials: materialsRows,
materials: materials,
installation_manuals: manualsRows,
usage_count: usageRows[0]?.usage_count || 0
};
@@ -1333,6 +1342,252 @@ class SmartPackageManagementService {
throw error;
}
}
/**
* Evaluer formel med geometri-data
* Understøtter formler som: "facadelængde/3", "(huslængde+1)*husbredde", "tagflade_m2*2.13"
*/
evaluateFormula(formula, geometry) {
try {
// Fjern "FORMULA:" prefix hvis det findes
let expr = formula.replace(/^FORMULA:/i, '').trim();
// Erstatter danske matematiske tegn med JavaScript operatorer
expr = expr.replace(/÷/g, '/'); // Division
expr = expr.replace(/×/g, '*'); // Multiplikation
expr = expr.replace(/,/g, '.'); // Komma til punktum for decimaler
logger.info('Evaluating formula:', { original: formula, cleaned: expr });
// Map geometri-værdier til variable
const variables = {
facadelængde: geometry.length_main || 0,
huslængde: geometry.length_main || 0,
husbredde: geometry.width_main || 0,
tagflade_m2: geometry.total_area || 0,
tagflade: geometry.total_area || 0,
rygning_længde: geometry.length_main || 0,
nedløb_antal: geometry.nedløb_antal || 1,
m2: geometry.total_area || 0,
stern_til_kip: geometry.stern_to_ridge_height || 0,
højde_stern_til_kip: geometry.stern_to_ridge_height || 0,
taghøjde: geometry.roof_height || 0
};
// Erstat variable i formlen
for (const [key, value] of Object.entries(variables)) {
const regex = new RegExp(key, 'gi');
expr = expr.replace(regex, value);
}
logger.info('After variable substitution:', { expr, geometry: { length: geometry.length_main, width: geometry.width_main, area: geometry.total_area } });
// Evaluer matematisk udtryk (sikker måde)
// Tillad kun tal, +, -, *, /, (, ), .
if (!/^[\d\s+\-*/.()]+$/.test(expr)) {
logger.warn('Invalid formula expression:', { expr, original: formula });
return 1; // Fallback
}
const result = eval(expr);
logger.info('Formula result:', { result, expr });
return Math.max(0, result); // Ingen negative værdier
} catch (error) {
logger.error('Error evaluating formula:', { formula, error: error.message });
return 1; // Fallback
}
}
/**
* Beregn materialer med formler baseret på geometri
* Bruges når SmartPackage tilføjes til projekt
* @param {number} packageId - ID på SmartPackage
* @param {object} geometry - Geometri data (length_main, width_main, total_area)
* @param {number} markupPercent - Material markup procent (standard 20%)
*/
async calculatePackageMaterialsWithGeometry(packageId, geometry, markupPercent = 20) {
try {
// Hent pakke med beskrivelse der indeholder formler
const [packageRows] = await this.db.pool.execute(`
SELECT * FROM material_packages WHERE id = ?
`, [packageId]);
if (packageRows.length === 0) {
throw new Error('Package not found');
}
const pkg = packageRows[0];
const description = pkg.description || '';
// Parse varenumre og formler fra beskrivelsen
const materialFormulas = this.parseFormulasFromDescription(description);
logger.info('Processing materials for package:', {
packageId,
packageName: pkg.name,
materialCount: materialFormulas.length,
markupPercent,
geometry: {
length: geometry.length_main,
width: geometry.width_main,
area: geometry.total_area
}
});
// Beregn quantities
const calculatedMaterials = materialFormulas.map(item => {
let quantity = 1;
let formulaUsed = 'fast antal';
if (item.fixedQuantity) {
// Fast antal fra beskrivelse (f.eks. "4 stk")
quantity = item.fixedQuantity;
formulaUsed = `fast antal: ${quantity}`;
} else if (item.formula) {
// Beregn fra formel
quantity = this.evaluateFormula(item.formula, geometry);
quantity = Math.ceil(quantity); // Rund op til nærmeste hele tal
formulaUsed = item.formula;
}
// Tilføj dynamisk avance på pris (hvis pris findes)
const markupMultiplier = 1 + (markupPercent / 100);
const priceWithMarkup = item.price ? item.price * markupMultiplier : 0;
logger.info('Calculated material:', {
name: item.name,
quantity,
basePrice: item.price,
markupPercent,
priceWithMarkup,
formula: item.formula,
fixedQuantity: item.fixedQuantity
});
return {
varenr: item.varenr,
name: item.name,
quantity,
base_price: item.price,
price: priceWithMarkup,
markup_percent: markupPercent,
formula_used: formulaUsed
};
});
return {
package: pkg,
materials: calculatedMaterials,
geometry_used: geometry,
markup_percent: markupPercent
};
} catch (error) {
logger.error('Error calculating materials with geometry:', error);
throw error;
}
}
/**
* Parse formler fra pakke-beskrivelse
* Format: "• 077424 - Tagrende 3m (beregnes fra facadelængde ÷ 3)"
*/
parseFormulasFromDescription(description) {
const materials = [];
const lines = description.split('\n');
for (const line of lines) {
// Match varenummer og navn - både med og uden punkt-tegn
const match = line.match(/[•·]\s*(\d+)\s*-\s*([^(]+)(?:\(([^)]+)\))?/);
if (match) {
const [, varenr, name, details] = match;
// Udtræk formel hvis den findes
let formula = null;
let fixedQuantity = null;
if (details) {
// Match forskellige formelformater
const patterns = [
/(?:antal|beregnes):\s*([^,)]+)/i, // "antal: facadelængde÷3"
/(?:beregnes fra|baseret på)\s+([^,)]+)/i, // "beregnes fra facadelængde ÷ 3"
/(\d+(?:\.\d+)?)\s+(?:pr|per)\s+(?:m²|m2)/i, // "2,13 pr m²"
/(\d+(?:\.\d+)?)\s+(?:pr|per)\s+(?:meter|m)/i, // "4 pr meter"
/(\d+)\s+stk/i // "4 stk"
];
for (const pattern of patterns) {
const formulaMatch = details.match(pattern);
if (formulaMatch) {
if (pattern.source.includes('stk')) {
// Fast antal
fixedQuantity = parseInt(formulaMatch[1]);
} else {
formula = formulaMatch[1].trim();
}
break;
}
}
}
materials.push({
varenr: varenr.trim(),
name: name.trim(),
formula: formula ? `FORMULA:${formula}` : null,
fixedQuantity,
price: 0 // Skal hentes fra materials tabel
});
}
}
logger.info('Parsed materials from description:', { count: materials.length, materials });
return materials;
}
/**
* Beregn arbejdstid med formel fra beskrivelse
* Format: "10 meter = 5 timer" eller "0,5 time pr m²"
*/
calculateLaborHours(description, geometry) {
try {
// Match forskellige formater
const patterns = [
/(\d+(?:\.\d+)?)\s*(?:meter|m)\s*=\s*(\d+(?:\.\d+)?)\s*time/i, // "10m = 5 timer"
/(\d+(?:,\d+)?)\s*time\s*pr\s*(?:m²|m2)/i, // "0,5 time pr m²"
/(\d+(?:,\d+)?)\s*time\s*pr\s*(?:meter|m)/i // "0,5 time pr meter"
];
for (const pattern of patterns) {
const match = description.match(pattern);
if (match) {
if (pattern.source.includes('=')) {
// Format: "10m = 5 timer"
const meters = parseFloat(match[1]);
const hours = parseFloat(match[2]);
const timePerMeter = hours / meters;
const totalMeters = geometry.length_main || 0;
return totalMeters * timePerMeter;
} else if (pattern.source.includes('m²|m2')) {
// Format: "0,5 time pr m²"
const hoursPerM2 = parseFloat(match[1].replace(',', '.'));
return (geometry.total_area || 0) * hoursPerM2;
} else {
// Format: "0,5 time pr meter"
const hoursPerMeter = parseFloat(match[1].replace(',', '.'));
return (geometry.length_main || 0) * hoursPerMeter;
}
}
}
// Fallback: returner estimated_hours fra pakken
return 0;
} catch (error) {
logger.error('Error calculating labor hours:', error);
return 0;
}
}
}
module.exports = SmartPackageManagementService;

View File

@@ -0,0 +1,517 @@
/**
* Script til at oprette erfaringsbaserede SmartPackages
* Baseret på kundens faktiske priser og erfaringer
*/
const axios = require('axios');
const mysql = require('mysql2/promise');
const LOCAL_API = 'http://localhost:4031';
// Database connection
const dbConfig = {
host: 'localhost',
user: 'tilbudgivern_service',
password: 'REDACTED_PASSWORD',
database: 'tilbudgivern'
};
/**
* Hent pris fra database med 20% avance
*/
async function getPriceWithMarkup(varenr) {
const connection = await mysql.createConnection(dbConfig);
try {
const [rows] = await connection.execute(
`SELECT mp.price, mp.unit, m.name
FROM materials m
LEFT JOIN material_prices mp ON m.id = mp.material_id
WHERE m.sku = ? AND mp.is_active = 1
ORDER BY mp.valid_from DESC LIMIT 1`,
[varenr]
);
if (rows.length > 0) {
const basePrice = parseFloat(rows[0].price);
const priceWithMarkup = basePrice * 1.20; // 20% avance
return {
price: priceWithMarkup,
unit: rows[0].unit,
name: rows[0].name
};
}
// Returner placeholder hvis ikke fundet - tilføjes senere
return null;
} finally {
await connection.end();
}
}
/**
* Opret materiale entry (bruges til at oprette pakkerne nu, priser tilføjes senere)
*/
function createMaterialEntry(name, productNumber, quantity, unit, description = '') {
return {
name,
productNumber,
quantity: typeof quantity === 'string' ? 1 : quantity,
quantityFormula: typeof quantity === 'string' ? quantity : null,
unit,
unitPrice: 0, // Placeholder - opdateres når priser tilføjes
description,
needsPriceUpdate: true
};
}
/**
* Opret SmartPackage via API
*/
async function createSmartPackage(packageData) {
try {
const response = await axios.post(`${LOCAL_API}/api/smart-packages`, packageData);
console.log(`✅ Oprettet pakke: ${packageData.name} (ID: ${response.data.id})`);
return response.data;
} catch (error) {
console.error(`❌ Fejl ved oprettelse af ${packageData.name}:`, error.response?.data || error.message);
throw error;
}
}
/**
* PAKKE 1: Tagrende med Konsoljern i stål (Lindab materialer)
*/
async function createTagrendePakke() {
console.log('\n📦 Opretter Tagrende pakke...');
// Materialeliste med formler for beregning
const materials = [
createMaterialEntry(
'Tagrende 3 meter',
'077424',
'FORMULA:facadelængde/3',
'stk',
'Antal beregnes automatisk: facadelængde / 3 meter'
),
createMaterialEntry(
'Ende bund',
'107744',
4,
'stk',
'1 i hver ende - 4 stk generisk'
),
createMaterialEntry(
'Samle stykke',
'077419',
'FORMULA:(facadelængde/3)-1',
'stk',
'1 samle beslag pr 3 meter tagrende'
),
createMaterialEntry(
'Konsol jern',
'077407',
'FORMULA:facadelængde/0.6',
'stk',
'Beregnes automatisk: maks 60cm afstand'
),
createMaterialEntry(
'Hjørne indvendig',
'077426',
1,
'stk',
'Huskeliste - tilpas efter behov'
),
createMaterialEntry(
'Hjørne udvendig',
'077431',
1,
'stk',
'Huskeliste - tilpas efter behov'
)
];
const packageData = {
name: 'Tagrende med Konsoljern (Lindab)',
description: 'Komplet tagrende system med konsoljern i stål. Materialer fra Lindab.\n\nArbeidsberegning: 10 meter tager 5 timer (0,5 time pr meter)',
category: 'Tag - Tagrende',
estimatedHours: 'FORMULA:facadelængde*0.5', // 10m = 5 timer
materials,
requiresGeometry: true,
geometryFields: ['facadelængde']
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 2: Nedløb
*/
async function createNedløbPakke() {
console.log('\n📦 Opretter Nedløb pakke...');
const materials = [
{
name: 'Nedløb 3 meter',
productNumber: '077438',
quantity: 'FORMULA:nedløb_antal', // Sættes af bruger
unit: 'stk'
},
{
name: 'Rørholder',
productNumber: '184937',
quantity: 'FORMULA:nedløb_antal*2', // 2 pr nedløb
unit: 'stk'
},
{
name: 'Tudstykke',
productNumber: '122214',
quantity: 'FORMULA:nedløb_antal', // 1 pr nedløb
unit: 'stk'
},
{
name: 'Silikone',
productNumber: '050043',
quantity: 'FORMULA:nedløb_antal', // 1 pr nedløb
unit: 'stk'
},
{
name: 'Brøndkrave',
productNumber: '061637',
quantity: 'FORMULA:nedløb_antal', // 1 pr nedløb
unit: 'stk'
},
{
name: 'Bøjning',
productNumber: '076623',
quantity: 'FORMULA:nedløb_antal*2', // 2 pr nedløb
unit: 'stk',
description: 'Bøjninger varierer - tilpas efter behov'
}
];
const materialsWithPrices = [];
for (const m of materials) {
const priceData = await getPriceWithMarkup(m.productNumber);
if (priceData) {
materialsWithPrices.push({
name: priceData.name || m.name,
quantity: 1, // Placeholder
quantityFormula: m.quantity,
unit: priceData.unit,
unitPrice: priceData.price,
description: m.description
});
} else {
console.warn(`⚠️ Kunne ikke finde pris for ${m.productNumber} - ${m.name}`);
}
}
const packageData = {
name: 'Nedløb komplet',
description: 'Komplet nedløbssystem med tilbehør.\n\nVælg antal nedløb - systemet beregner automatisk tilbehør.\n\nArbeidsberegning: 1 nedløb = 1 time',
category: 'Tag - Nedløb',
estimatedHours: 'FORMULA:nedløb_antal*1', // 1 stk = 1 time
materials: materialsWithPrices,
requiresGeometry: true,
geometryFields: ['nedløb_antal']
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 3: Opretning af spær
*/
async function createSpærPakke() {
console.log('\n📦 Opretter Opretning af spær pakke...');
const materials = [
{
name: 'Spær træ 200mm',
productNumber: '146304720010450',
quantity: 'FORMULA:(huslængde+1)*husbredde', // LBM beregning
unit: 'løbende meter'
},
{
name: 'Spånskruer 5,0x100',
productNumber: '310615',
quantity: 'FORMULA:((huslængde+1)*husbredde)*5', // 5 skruer pr LBM
unit: 'stk'
},
{
name: 'Muresnor',
productNumber: '195408',
quantity: 1,
unit: 'stk'
},
{
name: 'Bits TX 20',
productNumber: '224770',
quantity: 1,
unit: 'stk'
},
{
name: 'Snorvatterpas',
productNumber: '064736',
quantity: 2,
unit: 'stk'
},
{
name: 'Rundsavsklinge',
productNumber: '182788',
quantity: 1,
unit: 'stk'
},
{
name: 'Stiksavsklinger',
productNumber: '147761',
quantity: 1,
unit: 'stk'
}
];
const materialsWithPrices = [];
for (const m of materials) {
const priceData = await getPriceWithMarkup(m.productNumber);
if (priceData) {
materialsWithPrices.push({
name: priceData.name || m.name,
quantity: typeof m.quantity === 'string' ? 1 : m.quantity,
quantityFormula: typeof m.quantity === 'string' ? m.quantity : null,
unit: priceData.unit,
unitPrice: priceData.price
});
} else {
console.warn(`⚠️ Kunne ikke finde pris for ${m.productNumber} - ${m.name}`);
}
}
const packageData = {
name: 'Opretning af spær',
description: 'Komplet opretning af tagspær med materialer og værktøj.\n\nLBM beregnes automatisk: (huslængde + 1) × husbredde\n\nArbeidsberegning: 5 meter = 0,5 time',
category: 'Tag - Spær',
estimatedHours: 'FORMULA:((huslængde+1)*husbredde)*0.1', // 5m = 0.5t => 1m = 0.1t
materials: materialsWithPrices,
requiresGeometry: true,
geometryFields: ['huslængde', 'husbredde']
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 4: Montering af B7 inkl. lægter
*/
async function createB7Pakke() {
console.log('\n📦 Opretter B7 montering pakke...');
const materials = [
{
name: 'B7 Plade',
productNumber: '147072',
quantity: 'FORMULA:tagflade_m2*2.13', // 2,13 plade pr m²
unit: 'm²'
},
{
name: 'Lægter',
productNumber: '1632038073',
quantity: 'FORMULA:tagflade_m2*2.17', // 2,17 m pr m²
unit: 'løbende meter',
description: 'Lægteafstand 46 cm'
},
{
name: 'Tagskruer',
productNumber: '119229',
quantity: 'FORMULA:tagflade_m2*4.25', // 4,25 skrue pr m²
unit: 'stk'
},
{
name: 'Ringsøm',
productNumber: '101933',
quantity: 'FORMULA:tagflade_m2*15', // 15 søm pr m²
unit: 'stk',
description: 'Pakke med 2880 stk'
},
{
name: 'EPDM bånd 30 meter',
productNumber: '029218',
quantity: 'FORMULA:tagflade_m2*2.17', // 2,17 m pr m²
unit: 'løbende meter'
}
];
const materialsWithPrices = [];
for (const m of materials) {
const priceData = await getPriceWithMarkup(m.productNumber);
if (priceData) {
materialsWithPrices.push({
name: priceData.name || m.name,
quantity: typeof m.quantity === 'string' ? 1 : m.quantity,
quantityFormula: typeof m.quantity === 'string' ? m.quantity : null,
unit: priceData.unit,
unitPrice: priceData.price,
description: m.description
});
} else {
console.warn(`⚠️ Kunne ikke finde pris for ${m.productNumber} - ${m.name}`);
}
}
const packageData = {
name: 'B7 montering inkl. lægter',
description: 'Komplet B7 tagbeklædning med lægter og fastgørelse.\n\nArbeidsberegning: 0,5 time pr m²',
category: 'Tag - B7',
estimatedHours: 'FORMULA:tagflade_m2*0.5', // 0,5 time pr m²
materials: materialsWithPrices,
requiresGeometry: true,
geometryFields: ['tagflade_m2']
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 5: Montering af rygning B7
*/
async function createRygningB7Pakke() {
console.log('\n📦 Opretter Rygning B7 pakke...');
const materials = [
{
name: 'Rygning',
productNumber: '057437',
quantity: 'FORMULA:rygning_længde', // 1 stk pr meter
unit: 'løbende meter'
},
{
name: 'Rygningselement',
productNumber: '038218',
quantity: 'FORMULA:rygning_længde', // 1 stk pr meter
unit: 'løbende meter'
},
{
name: 'Tagskruer 100 stk',
productNumber: '144855',
quantity: 'FORMULA:rygning_længde*4', // 4 skrue pr meter
unit: 'stk'
},
{
name: 'EPDM bånd 30 meter',
productNumber: '029218',
quantity: 'FORMULA:rygning_længde*0.4', // 0,4 meter pr meter
unit: 'løbende meter'
}
];
const materialsWithPrices = [];
for (const m of materials) {
const priceData = await getPriceWithMarkup(m.productNumber);
if (priceData) {
materialsWithPrices.push({
name: priceData.name || m.name,
quantity: typeof m.quantity === 'string' ? 1 : m.quantity,
quantityFormula: typeof m.quantity === 'string' ? m.quantity : null,
unit: priceData.unit,
unitPrice: priceData.price
});
} else {
console.warn(`⚠️ Kunne ikke finde pris for ${m.productNumber} - ${m.name}`);
}
}
const packageData = {
name: 'Rygning B7',
description: 'Komplet rygningsløsning til B7 tag.\n\nArbeidsberegning: Tilføjes senere',
category: 'Tag - Rygning',
estimatedHours: 'FORMULA:rygning_længde*0.25', // Estimeret 15 min pr meter
materials: materialsWithPrices,
requiresGeometry: true,
geometryFields: ['rygning_længde']
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 6: Montering af taghætte B7
*/
async function createTaghætteB7Pakke() {
console.log('\n📦 Opretter Taghætte B7 pakke...');
const materials = [
{
name: 'Taghætte',
productNumber: '126813',
quantity: 1,
unit: 'stk'
}
];
const materialsWithPrices = [];
for (const m of materials) {
const priceData = await getPriceWithMarkup(m.productNumber);
if (priceData) {
materialsWithPrices.push({
name: priceData.name || m.name,
quantity: m.quantity,
unit: priceData.unit,
unitPrice: priceData.price
});
} else {
console.warn(`⚠️ Kunne ikke finde pris for ${m.productNumber} - ${m.name}`);
}
}
const packageData = {
name: 'Taghætte B7',
description: 'Montering af taghætte til B7 tag.\n\nArbeidsberegning: Tilføjes senere',
category: 'Tag - Taghætte',
estimatedHours: 1, // Estimeret
materials: materialsWithPrices,
requiresGeometry: false
};
return await createSmartPackage(packageData);
}
/**
* Hovedfunktion
*/
async function main() {
console.log('🚀 Starter oprettelse af erfaringsbaserede SmartPackages...\n');
console.log('📌 Alle priser hentes fra database med 20% avance\n');
try {
// Opret alle pakker
await createTagrendePakke();
await createNedløbPakke();
await createSpærPakke();
await createB7Pakke();
await createRygningB7Pakke();
await createTaghætteB7Pakke();
console.log('\n✅ Alle pakker oprettet succesfuldt!');
console.log('\n📝 Næste skridt:');
console.log(' 1. Sikr at geometri gemmes i database (facadelængde, husbredde, m² osv.)');
console.log(' 2. Implementer beregningslogik for formler (FORMULA:...)');
console.log(' 3. Test pakkerne i frontend');
console.log(' 4. Få manglende pakker fra kunde (B6, Betontegl, Velux osv.)');
} catch (error) {
console.error('\n❌ Fejl under oprettelse:', error);
process.exit(1);
}
}
// Kør scriptet
if (require.main === module) {
main();
}
module.exports = {
createTagrendePakke,
createNedløbPakke,
createSpærPakke,
createB7Pakke,
createRygningB7Pakke,
createTaghætteB7Pakke
};

265
create-packages-simple.js Normal file
View File

@@ -0,0 +1,265 @@
/**
* Script til at oprette erfaringsbaserede SmartPackages
* Version 2: Simpel version der opretter pakkerne med varenumre
* Priser tilføjes senere når materialer importeres fra Bygma/leverandører
*/
const axios = require('axios');
const LOCAL_API = 'http://localhost:4031';
/**
* Opret SmartPackage via API
*/
async function createSmartPackage(packageData) {
try {
const response = await axios.post(`${LOCAL_API}/api/smart-packages`, packageData);
console.log(`✅ Oprettet pakke: ${packageData.name} (ID: ${response.data.id})`);
return response.data;
} catch (error) {
console.error(`❌ Fejl ved oprettelse af ${packageData.name}:`, error.response?.data || error.message);
return null;
}
}
/**
* PAKKE 1: Tagrende med Konsoljern
*/
async function createTagrendePakke() {
const packageData = {
name: 'Tagrende med Konsoljern (Lindab)',
description: `Komplet tagrende system med konsoljern i stål fra Lindab.
MATERIALELISTE:
• 077424 - Tagrende 3 meter (beregnes fra facadelængde ÷ 3)
• 107744 - Ende bund (4 stk generisk)
• 077419 - Samle stykke (1 pr 3 meter tagrende)
• 077407 - Konsol jern (maks 60cm mellem)
• 077426 - Hjørne indvendig (1 stk til huskeliste)
• 077431 - Hjørne udvendig (1 stk til huskeliste)
ARBEJDSTID: 10 meter = 5 timer (0,5 time pr meter)
GEOMETRI KRÆVET: facadelængde (meter)`,
category: 'Tag - Tagrende',
difficulty_level: 'mellem',
estimated_hours: 5,
hourly_rate: 580,
installation_notes: 'Kræver facadelængde for beregning',
is_template: true,
created_by: 'system',
materials: [
{ name: 'Tagrende 3 meter (077424)', quantity: 1, unit: 'stk', unitPrice: 0, category: 'tagrende', supplier: '', notes: '' },
{ name: 'Ende bund (107744)', quantity: 4, unit: 'stk', unitPrice: 0, category: 'tagrende', supplier: '', notes: '' },
{ name: 'Samle stykke (077419)', quantity: 1, unit: 'stk', unitPrice: 0, category: 'tagrende', supplier: '', notes: '' },
{ name: 'Konsol jern (077407)', quantity: 1, unit: 'stk', unitPrice: 0, category: 'tagrende', supplier: '', notes: '' },
{ name: 'Hjørne indvendig (077426)', quantity: 1, unit: 'stk', unitPrice: 0, category: 'tagrende', supplier: '', notes: '' },
{ name: 'Hjørne udvendig (077431)', quantity: 1, unit: 'stk', unitPrice: 0, category: 'tagrende', supplier: '', notes: '' }
]
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 2: Nedløb
*/
async function createNedløbPakke() {
const packageData = {
name: 'Nedløb komplet',
description: `Komplet nedløbssystem med automatisk beregning af tilbehør.
MATERIALELISTE (pr nedløb):
• 077438 - Nedløb 3 meter (1 stk)
• 184937 - Rørholder (2 stk)
• 122214 - Tudstykke (1 stk)
• 050043 - Silikone (1 stk)
• 061637 - Brøndkrave (1 stk)
• 076623 - Bøjning (2 stk - varierer efter behov)
ARBEJDSTID: 1 nedløb = 1 time
GEOMETRI KRÆVET: Antal nedløb`,
category: 'Tag - Nedløb',
package_type: 'installation',
estimated_hours: 1,
hourly_rate: 580,
is_template: true,
materials: [
{ name: 'Nedløb 3 meter (077438)', quantity: 1, unit: 'stk', price: 0 },
{ name: 'Rørholder (184937)', quantity: 2, unit: 'stk', price: 0 },
{ name: 'Tudstykke (122214)', quantity: 1, unit: 'stk', price: 0 },
{ name: 'Silikone (050043)', quantity: 1, unit: 'stk', price: 0 },
{ name: 'Brøndkrave (061637)', quantity: 1, unit: 'stk', price: 0 },
{ name: 'Bøjning (076623)', quantity: 2, unit: 'stk', price: 0 }
]
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 3: Opretning af spær
*/
async function createSpærPakke() {
const packageData = {
name: 'Opretning af spær',
description: `Komplet opretning af tagspær med materialer og værktøj.
MATERIALELISTE:
• 146304720010450 - Spær træ 200mm (LBM: (huslængde+1) × husbredde)
• 310615 - Spånskruer 5,0x100 (5 skruer pr LBM)
• 195408 - Muresnor (1 stk)
• 224770 - Bits TX 20 (1 stk)
• 064736 - Snorvatterpas (2 stk)
• 182788 - Rundsavsklinge (1 stk)
• 147761 - Stiksavsklinger (1 stk)
ARBEJDSTID: 5 meter = 0,5 time (0,1 time pr meter)
GEOMETRI KRÆVET: huslængde (meter), husbredde (meter)`,
category: 'Tag - Spær',
package_type: 'installation',
estimated_hours: 2.5,
hourly_rate: 580,
is_template: true,
materials: [
{ name: 'Spær træ 200mm (146304720010450)', quantity: 10, unit: 'løbende meter', price: 0 },
{ name: 'Spånskruer 5,0x100 (310615)', quantity: 50, unit: 'stk', price: 0 },
{ name: 'Muresnor (195408)', quantity: 1, unit: 'stk', price: 0 },
{ name: 'Bits TX 20 (224770)', quantity: 1, unit: 'stk', price: 0 },
{ name: 'Snorvatterpas (064736)', quantity: 2, unit: 'stk', price: 0 },
{ name: 'Rundsavsklinge (182788)', quantity: 1, unit: 'stk', price: 0 },
{ name: 'Stiksavsklinger (147761)', quantity: 1, unit: 'stk', price: 0 }
]
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 4: B7 montering inkl. lægter
*/
async function createB7Pakke() {
const packageData = {
name: 'B7 montering inkl. lægter',
description: `Komplet B7 tagbeklædning med lægter og fastgørelse.
MATERIALELISTE (pr m²):
• 147072 - B7 Plade (2,13 plade pr m²)
• 1632038073 - Lægter 46cm afstand (2,17 meter pr m²)
• 119229 - Tagskruer (4,25 skruer pr m²)
• 101933 - Ringsøm pakke 2880 stk (15 søm pr m²)
• 029218 - EPDM bånd 30 meter (2,17 meter pr m²)
ARBEJDSTID: 0,5 time pr m²
GEOMETRI KRÆVET: tagflade (m²)`,
category: 'Tag - B7',
package_type: 'installation',
estimated_hours: 10,
hourly_rate: 580,
is_template: true,
materials: [
{ name: 'B7 Plade (147072)', quantity: 21.3, unit: 'stk', price: 0, description: '2,13 pr m²' },
{ name: 'Lægter (1632038073)', quantity: 21.7, unit: 'løbende meter', price: 0, description: '2,17m pr m²' },
{ name: 'Tagskruer (119229)', quantity: 42.5, unit: 'stk', price: 0, description: '4,25 pr m²' },
{ name: 'Ringsøm (101933)', quantity: 150, unit: 'stk', price: 0, description: '15 pr m²' },
{ name: 'EPDM bånd (029218)', quantity: 21.7, unit: 'løbende meter', price: 0, description: '2,17m pr m²' }
]
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 5: Rygning B7
*/
async function createRygningB7Pakke() {
const packageData = {
name: 'Rygning B7',
description: `Komplet rygningsløsning til B7 tag.
MATERIALELISTE (pr løbende meter):
• 057437 - Rygning (1 stk pr meter)
• 038218 - Rygningselement (1 stk pr meter)
• 144855 - Tagskruer 100 stk (4 skruer pr meter)
• 029218 - EPDM bånd 30 meter (0,4 meter pr meter)
ARBEJDSTID: Ca 0,25 time pr meter (estimeret)
GEOMETRI KRÆVET: rygning_længde (meter)`,
category: 'Tag - Rygning',
package_type: 'installation',
estimated_hours: 2.5,
hourly_rate: 580,
is_template: true,
materials: [
{ name: 'Rygning (057437)', quantity: 10, unit: 'løbende meter', price: 0 },
{ name: 'Rygningselement (038218)', quantity: 10, unit: 'løbende meter', price: 0 },
{ name: 'Tagskruer (144855)', quantity: 40, unit: 'stk', price: 0, description: '4 pr meter' },
{ name: 'EPDM bånd (029218)', quantity: 4, unit: 'løbende meter', price: 0, description: '0,4m pr meter' }
]
};
return await createSmartPackage(packageData);
}
/**
* PAKKE 6: Taghætte B7
*/
async function createTaghætteB7Pakke() {
const packageData = {
name: 'Taghætte B7',
description: `Montering af taghætte til B7 tag.
MATERIALELISTE:
• 126813 - Taghætte (1 stk)
ARBEJDSTID: Ca 1 time (estimeret)`,
category: 'Tag - Taghætte',
package_type: 'installation',
estimated_hours: 1,
hourly_rate: 580,
is_template: true,
materials: [
{ name: 'Taghætte (126813)', quantity: 1, unit: 'stk', price: 0 }
]
};
return await createSmartPackage(packageData);
}
/**
* Hovedfunktion
*/
async function main() {
console.log('🚀 Starter oprettelse af erfaringsbaserede SmartPackages...\n');
console.log('📌 Pakker oprettes med varenumre - priser tilføjes senere\n');
const results = [];
// Opret alle pakker
results.push(await createTagrendePakke());
results.push(await createNedløbPakke());
results.push(await createSpærPakke());
results.push(await createB7Pakke());
results.push(await createRygningB7Pakke());
results.push(await createTaghætteB7Pakke());
const successful = results.filter(r => r !== null).length;
console.log(`\n${successful} af 6 pakker oprettet succesfuldt!`);
console.log('\n📝 Næste skridt:');
console.log(' 1. Import materialer fra Bygma/leverandører med varenumre');
console.log(' 2. Implementer geometri-gemning i database');
console.log(' 3. Byg beregningslogik for automatisk materialemængder');
console.log(' 4. Implementer 20% avance på alle materialerpriser');
console.log(' 5. Test pakkerne i frontend');
console.log(' 6. Få manglende pakker fra kunde (B6, Betontegl, Velux osv.)');
}
// Kør scriptet
if (require.main === module) {
main();
}

171
create-packages-v3.js Normal file
View File

@@ -0,0 +1,171 @@
/**
* Script til at oprette basis erfaringspakker
* Minimal version - kun nødvendige felter
*/
const axios = require('axios');
const LOCAL_API = 'http://localhost:4031';
async function createPackage(data) {
try {
const res = await axios.post(`${LOCAL_API}/api/smart-packages`, data);
console.log(`${data.name} (ID: ${res.data.package.id})`);
return res.data;
} catch (error) {
console.error(`${data.name}:`, error.response?.data?.error || error.message);
return null;
}
}
async function main() {
console.log('🚀 Opretter 6 erfaringsbaserede pakker...\n');
const packages = [
{
name: 'Tagrende med Konsoljern (Lindab)',
description: `Komplet tagrende system - Lindab materialer
VARENUMRE:
• 077424 - Tagrende 3m (antal: facadelængde÷3)
• 107744 - Ende bund (4 stk)
• 077419 - Samle stykke (1 pr 3m)
• 077407 - Konsol jern (max 60cm afstand)
• 077426 - Hjørne indvendig (1 stk)
• 077431 - Hjørne udvendig (1 stk)
ARBEJDSTID: 10m = 5 timer`,
category: 'Tag - Tagrende',
difficulty_level: 'mellem',
estimated_hours: 5,
hourly_rate: 580,
installation_notes: 'Beregning baseret på facadelængde',
is_template: true,
created_by: 'system',
materials: [],
tasks: []
},
{
name: 'Nedløb komplet',
description: `Komplet nedløbssystem med tilbehør
VARENUMRE (pr nedløb):
• 077438 - Nedløb 3m (1 stk)
• 184937 - Rørholder (2 stk)
• 122214 - Tudstykke (1 stk)
• 050043 - Silikone (1 stk)
• 061637 - Brøndkrave (1 stk)
• 076623 - Bøjning (2 stk)
ARBEJDSTID: 1 nedløb = 1 time`,
category: 'Tag - Nedløb',
difficulty_level: 'let',
estimated_hours: 1,
hourly_rate: 580,
installation_notes: 'Tilbehør ganges automatisk med antal nedløb',
is_template: true,
created_by: 'system',
materials: [],
tasks: []
},
{
name: 'Opretning af spær',
description: `Opretning af tagspær inkl. værktøj
VARENUMRE:
• 146304720010450 - Spær træ 200mm (LBM: (huslængde+1)×husbredde)
• 310615 - Spånskruer 5,0x100 (5 pr LBM)
• 195408 - Muresnor (1 stk)
• 224770 - Bits TX 20 (1 stk)
• 064736 - Snorvatterpas (2 stk)
• 182788 - Rundsavsklinge (1 stk)
• 147761 - Stiksavsklinger (1 stk)
ARBEJDSTID: 5m = 0,5 time`,
category: 'Tag - Spær',
difficulty_level: 'svær',
estimated_hours: 2.5,
hourly_rate: 580,
installation_notes: 'LBM = Løbende meter. Beregnes fra husdimensioner',
is_template: true,
created_by: 'system',
materials: [],
tasks: []
},
{
name: 'B7 montering inkl. lægter',
description: `Komplet B7 tagbeklædning med lægter
VARENUMRE (pr m²):
• 147072 - B7 Plade (2,13 pr m²)
• 1632038073 - Lægter 46cm (2,17m pr m²)
• 119229 - Tagskruer (4,25 pr m²)
• 101933 - Ringsøm (15 pr m²)
• 029218 - EPDM bånd (2,17m pr m²)
ARBEJDSTID: 0,5 time pr m²`,
category: 'Tag - B7',
difficulty_level: 'mellem',
estimated_hours: 10,
hourly_rate: 580,
installation_notes: 'Beregnes ud fra tagflade i m²',
is_template: true,
created_by: 'system',
materials: [],
tasks: []
},
{
name: 'Rygning B7',
description: `Rygningsløsning til B7 tag
VARENUMRE (pr løbende meter):
• 057437 - Rygning (1 pr m)
• 038218 - Rygningselement (1 pr m)
• 144855 - Tagskruer (4 pr m)
• 029218 - EPDM bånd (0,4m pr m)
ARBEJDSTID: Ca 0,25 time pr meter`,
category: 'Tag - Rygning',
difficulty_level: 'mellem',
estimated_hours: 2.5,
hourly_rate: 580,
installation_notes: 'Beregnes ud fra rygningens længde',
is_template: true,
created_by: 'system',
materials: [],
tasks: []
},
{
name: 'Taghætte B7',
description: `Montering af taghætte til B7 tag
VARENUMRE:
• 126813 - Taghætte (1 stk)
ARBEJDSTID: Ca 1 time`,
category: 'Tag - Taghætte',
difficulty_level: 'let',
estimated_hours: 1,
hourly_rate: 580,
installation_notes: 'Standard installation',
is_template: true,
created_by: 'system',
materials: [],
tasks: []
}
];
let success = 0;
for (const pkg of packages) {
const result = await createPackage(pkg);
if (result) success++;
}
console.log(`\n${success}/${packages.length} pakker oprettet`);
console.log('\n📝 Næste skridt:');
console.log('1. Tjek pakkerne i frontend');
console.log('2. Import materialer fra Bygma med varenumre');
console.log('3. Implementer geometri-gemning');
console.log('4. Byg beregningslogik');
}
main();

View File

@@ -5,11 +5,58 @@
const axios = require('axios');
const graphqlClient = require('./backend/src/services/graphqlClient');
const mysql = require('mysql2/promise');
const GRAPHQL_ENDPOINT = 'https://graphql.ordrestyring.dk/graphql';
const API_TOKEN = process.env.ORDRESTYRING_API_TOKEN || 'cRD7xfeoiGh1OhzV';
const LOCAL_API = 'http://localhost:4031';
// Database connection
const dbConfig = {
host: 'localhost',
user: 'tilbudgivern_service',
password: 'REDACTED_PASSWORD',
database: 'tilbudgivern'
};
/**
* Hent pris fra database baseret på materiale navn
*/
async function getPriceFromDatabase(materialName, unit = null) {
const connection = await mysql.createConnection(dbConfig);
try {
const [rows] = await connection.execute(
`SELECT price, unit FROM material_prices
WHERE name LIKE ? AND is_active = 1
ORDER BY valid_from DESC LIMIT 1`,
[`%${materialName}%`]
);
if (rows.length > 0) {
return { price: parseFloat(rows[0].price), unit: rows[0].unit };
}
return null;
} finally {
await connection.end();
}
}
/**
* Opret materiale med database-pris hvis muligt
*/
async function createMaterial(name, quantity, unit, fallbackPrice, searchTerm = null) {
const dbPrice = await getPriceFromDatabase(searchTerm || name);
const unitPrice = dbPrice ? dbPrice.price : fallbackPrice;
const actualUnit = dbPrice ? dbPrice.unit : unit;
return {
name,
quantity,
unit: actualUnit,
unitPrice
};
}
// Query til at søge efter tag-relaterede sager
const SEARCH_CASES_QUERY = `
query SearchCases {
@@ -155,7 +202,7 @@ async function getCaseDetails(caseId) {
/**
* Analyser en sag og opret SmartPackage data
*/
function analyzeCaseForSmartPackage(caseData, caseDetails) {
async function analyzeCaseForSmartPackage(caseData, caseDetails) {
const description = (caseData.description || '').toLowerCase();
const remarks = (caseData.remarks || '').toLowerCase();
const workDone = (caseData.workDone || '').toLowerCase();
@@ -181,10 +228,10 @@ function analyzeCaseForSmartPackage(caseData, caseDetails) {
if (fullText.includes('tagpap') || fullText.includes('pap')) {
packageName = 'Tagpap - Renovering';
materials = [
{ name: 'Tagpap S2350', quantity: 1, unit: 'rulle', unitPrice: 850 },
{ name: 'Tagpaplim', quantity: 1, unit: 'spand', unitPrice: 450 },
{ name: 'Tagpapsøm', quantity: 1, unit: 'pakke', unitPrice: 120 },
{ name: 'Underlagspapir', quantity: 1, unit: 'rulle', unitPrice: 320 }
await createMaterial('Tagpap S2350', 10, 'm²', 850, 'tagpap'),
await createMaterial('Tagpaplim', 1, 'spand', 450, 'lim'),
await createMaterial('Tagpapsøm', 1, 'pakke', 120, 'søm'),
await createMaterial('Underlagspapir', 10, 'm²', 320, 'undertag')
];
tasks = [
{ name: 'Nedtagning af tagpap', description: 'Fjernelse af gammelt tagpap og forberedelse', hours: 2.0, order: 1 },
@@ -195,10 +242,10 @@ function analyzeCaseForSmartPackage(caseData, caseDetails) {
} else if (fullText.includes('tegl') || fullText.includes('tagsten')) {
packageName = 'Teglsten - Udskiftning';
materials = [
{ name: 'Teglsten', quantity: 30, unit: 'stk', unitPrice: 25 },
{ name: 'Tagmørtel', quantity: 1, unit: 'sæk', unitPrice: 180 },
{ name: 'Tagkroge', quantity: 30, unit: 'stk', unitPrice: 2 },
{ name: 'Lægter', quantity: 10, unit: 'meter', unitPrice: 35 }
await createMaterial('Tagsten beslag', 30, 'stk', 25, 'tagsten beslag'),
await createMaterial('Eternit tagsten B6', 5, 'm²', 145, 'eternit tagsten'),
await createMaterial('Tagmørtel', 1, 'sæk', 180, 'mørtel'),
await createMaterial('Lægter', 10, 'meter', 35, 'lægte')
];
tasks = [
{ name: 'Nedtagning af teglsten', description: 'Nedtagning af beskadigede teglsten', hours: 3.0, order: 1 },
@@ -206,74 +253,76 @@ function analyzeCaseForSmartPackage(caseData, caseDetails) {
{ name: 'Montering af teglsten', description: 'Montering af nye teglsten', hours: 5.0, order: 3 },
{ name: 'Kontrol og finish', description: 'Kontrol og efterjustering', hours: 1.5, order: 4 }
];
} else if (fullText.includes('stråtag') || fullText.includes('stråtækning')) {
} else if (fullText.includes('strå') || fullText.includes('thatched')) {
packageName = 'Stråtag - Renovering';
materials = [
{ name: 'Strå (langhalm)', quantity: 50, unit: 'knippe', price: 80 },
{ name: 'Bindetråd', quantity: 5, unit: 'rulle', price: 150 },
{ name: 'Stråkamme', quantity: 1, unit: 'stk', price: 450 }
{ name: 'Strå (langhalm)', quantity: 50, unit: 'knippe', unitPrice: 80 },
{ name: 'Bindetråd', quantity: 5, unit: 'rulle', unitPrice: 150 },
{ name: 'Stråkamme', quantity: 1, unit: 'stk', unitPrice: 450 },
{ name: 'Undertag og lægter', quantity: 1, unit: 'sæt', unitPrice: 850 }
];
tasks = [
{ name: 'Fjernelse af gammel strå', description: 'Fjernelse af gammel strå', hours: 2.0, order: 1 },
{ name: 'Kontrol af tagkonstruktion', description: 'Kontrol af tagkonstruktion', hours: 0.5, order: 2 },
{ name: 'Lægning af nyt strå', description: 'Lægning af nyt strå', hours: 4.0, order: 3 },
{ name: 'Tilskæring og finish', description: 'Tilskæring og finish', hours: 1.0, order: 4 }
{ name: 'Fjernelse af gammel strå', description: 'Fjernelse af gammel strå', hours: 4.0, order: 1 },
{ name: 'Kontrol af tagkonstruktion', description: 'Kontrol af tagkonstruktion', hours: 2.0, order: 2 },
{ name: 'Lægning af nyt strå', description: 'Lægning af nyt strå', hours: 12.0, order: 3 },
{ name: 'Tilskæring og finish', description: 'Tilskæring og finish', hours: 3.0, order: 4 }
];
} else if (fullText.includes('zink') || fullText.includes('metal')) {
packageName = 'Zinktag - Renovering';
materials = [
{ name: 'Zinkplader', quantity: 10, unit: 'm²', price: 380 },
{ name: 'Falsliste', quantity: 20, unit: 'meter', price: 45 },
{ name: 'Zinkskruer', quantity: 1, unit: 'pakke', price: 180 }
await createMaterial('Tagplade stål sort', 10, 'm²', 185, 'tagplade'),
await createMaterial('Falsliste', 20, 'meter', 45, 'fals'),
await createMaterial('Skruer til tagplader', 1, 'pakke', 125, 'skruer'),
await createMaterial('Undertag diffusionsåben', 10, 'm²', 42, 'undertag')
];
tasks = [
{ name: 'Nedtagning af zinkplader', description: 'Nedtagning af gamle zinkplader', hours: 1.0, order: 1 },
{ name: 'Reparation af underlag', description: 'Kontrol og reparation af underlag', hours: 0.5, order: 2 },
{ name: 'Montering af zinkplader', description: 'Montering af nye zinkplader', hours: 2.0, order: 3 },
{ name: 'Lodning og tætning', description: 'Lodning og tætning', hours: 1.0, order: 4 }
{ name: 'Nedtagning af zinkplader', description: 'Nedtagning af gamle zinkplader', hours: 2.5, order: 1 },
{ name: 'Reparation af underlag', description: 'Kontrol og reparation af underlag', hours: 2.0, order: 2 },
{ name: 'Montering af zinkplader', description: 'Montering af nye zinkplader', hours: 6.0, order: 3 },
{ name: 'Lodning og tætning', description: 'Lodning og tætning', hours: 3.0, order: 4 }
];
} else if (fullText.includes('rende') || fullText.includes('tagrende')) {
packageName = 'Tagrende - Renovering';
materials = [
{ name: 'Tagrende (plastik)', quantity: 10, unit: 'meter', price: 85 },
{ name: 'Rendebeslag', quantity: 15, unit: 'stk', price: 25 },
{ name: 'Rendeknæ', quantity: 4, unit: 'stk', price: 45 },
{ name: 'Nedløbsrør', quantity: 6, unit: 'meter', price: 120 }
await createMaterial('Tagrender halvrund zink', 10, 'meter', 245, 'tagrender'),
await createMaterial('Rendebeslag', 15, 'stk', 25, 'beslag'),
await createMaterial('Rendeknæ', 4, 'stk', 45, 'knæ'),
await createMaterial('Nedløbsrør zink', 6, 'meter', 195, 'nedløbsrør')
];
tasks = [
{ name: 'Nedtagning af tagrende', description: 'Nedtagning af gammel tagrende', hours: 1.0, order: 1 },
{ name: 'Justering af beslag', description: 'Kontrol og justering af beslag', hours: 0.5, order: 2 },
{ name: 'Montering af tagrende', description: 'Montering af ny tagrende', hours: 1.5, order: 3 },
{ name: 'Montering af nedløb', description: 'Montering af nedløb og afslutning', hours: 0.8, order: 4 }
{ name: 'Nedtagning af tagrende', description: 'Nedtagning af gammel tagrende', hours: 2.0, order: 1 },
{ name: 'Justering af beslag', description: 'Kontrol og justering af beslag', hours: 1.5, order: 2 },
{ name: 'Montering af tagrende', description: 'Montering af ny tagrende', hours: 3.5, order: 3 },
{ name: 'Montering af nedløb', description: 'Montering af nedløb og afslutning', hours: 2.0, order: 4 }
];
} else if (fullText.includes('skorsten') || fullText.includes('kvist')) {
packageName = 'Skorsten/Kvist - Renovering';
materials = [
{ name: 'Zinkblik til inddækning', quantity: 2, unit: 'm²', price: 450 },
{ name: 'Blikskruer', quantity: 1, unit: 'pakke', price: 95 },
{ name: 'Tætningsbånd', quantity: 5, unit: 'meter', price: 65 },
{ name: 'Fugmasse', quantity: 1, unit: 'tube', price: 120 }
await createMaterial('Zinkblik til inddækning', 2, 'm²', 450, 'zink'),
await createMaterial('Blikskruer', 1, 'pakke', 95, 'skruer'),
await createMaterial('Tætningsbånd', 5, 'meter', 65, 'tætning'),
await createMaterial('Fugmasse', 1, 'tube', 120, 'fuge')
];
tasks = [
{ name: 'Fjernelse af gammelt blik', description: 'Fjernelse af gammelt blik', hours: 0.8, order: 1 },
{ name: 'Rengøring og forberedelse', description: 'Rengøring og forberedelse', hours: 0.5, order: 2 },
{ name: 'Montering af zinkblik', description: 'Montering af nyt zinkblik', hours: 2.0, order: 3 },
{ name: 'Tætning og afslutning', description: 'Tætning og afslutning', hours: 1.0, order: 4 }
{ name: 'Fjernelse af gammelt blik', description: 'Fjernelse af gammelt blik', hours: 2.0, order: 1 },
{ name: 'Rengøring og forberedelse', description: 'Rengøring og forberedelse', hours: 1.5, order: 2 },
{ name: 'Montering af zinkblik', description: 'Montering af nyt zinkblik', hours: 4.5, order: 3 },
{ name: 'Tætning og afslutning', description: 'Tætning og afslutning', hours: 2.0, order: 4 }
];
} else {
// Generel tag-pakke
packageName = 'Tag - Generel reparation';
materials = [
{ name: 'Diverse tagreparationsmaterialer', quantity: 1, unit: 'sæt', price: 850 },
{ name: 'Tagpap (reserve)', quantity: 1, unit: 'rulle', price: 380 },
{ name: 'Skruer og beslag', quantity: 1, unit: 'sæt', price: 250 },
{ name: 'Tætningsprodukter', quantity: 1, unit: 'sæt', price: 450 }
await createMaterial('Tagpap underlag', 5, 'm²', 35.5, 'tagpap'),
await createMaterial('Undertag diffusionsåben', 5, 'm²', 42, 'undertag'),
await createMaterial('Skruer til tagplader', 1, 'pakke', 125, 'skruer'),
await createMaterial('Tagisolering mineraluld', 5, 'm²', 85, 'isolering')
];
tasks = [
{ name: 'Inspektion af tagskade', description: 'Inspektion og vurdering af tagskade', hours: 0.5, order: 1 },
{ name: 'Forberedende arbejde', description: 'Forberedende arbejde og sikring', hours: 0.5, order: 2 },
{ name: 'Tagreparation', description: 'Udførelse af tagreparation', hours: 2.0, order: 3 },
{ name: 'Oprydning og kontrol', description: 'Oprydning og kvalitetskontrol', hours: 0.5, order: 4 }
{ name: 'Inspektion af tagskade', description: 'Inspektion og vurdering af tagskade', hours: 1.5, order: 1 },
{ name: 'Forberedende arbejde', description: 'Forberedende arbejde og sikring', hours: 2.0, order: 2 },
{ name: 'Tagreparation', description: 'Udførelse af tagreparation', hours: 5.0, order: 3 },
{ name: 'Oprydning og kontrol', description: 'Oprydning og kvalitetskontrol', hours: 1.5, order: 4 }
];
}
@@ -285,7 +334,7 @@ function analyzeCaseForSmartPackage(caseData, caseDetails) {
: 580;
// Beregn total materialeomkostning
const totalMaterialCost = materials.reduce((sum, m) => sum + (m.price * m.quantity), 0);
const totalMaterialCost = materials.reduce((sum, m) => sum + (m.unitPrice * m.quantity), 0);
const totalTaskHours = tasks.reduce((sum, t) => sum + t.hours, 0);
// Format materials med unit_price i stedet for price
@@ -293,7 +342,7 @@ function analyzeCaseForSmartPackage(caseData, caseDetails) {
name: m.name,
quantity: m.quantity,
unit: m.unit,
unitPrice: m.price,
unitPrice: m.unitPrice,
category: 'Byggematerialer',
supplier: '',
notes: ''
@@ -333,7 +382,7 @@ function analyzeCaseForSmartPackage(caseData, caseDetails) {
async function createSmartPackage(packageData) {
try {
const response = await axios.post(
`${LOCAL_API}/api/smart-packages`,
'http://localhost:4031/api/smart-packages',
packageData,
{
headers: {
@@ -401,7 +450,7 @@ async function main() {
const caseDetails = await getCaseDetails(caseData.id);
// Analyser og opret package data
const packageData = analyzeCaseForSmartPackage(caseData, caseDetails);
const packageData = await analyzeCaseForSmartPackage(caseData, caseDetails);
// Opret SmartPackage
const packageId = await createSmartPackage(packageData);

View File

@@ -75,13 +75,16 @@ const MaterialDialog = ({ open, onClose, onSave, material, availableMaterials })
setShowManual(false);
if (value) {
// Brug prisen fra material_prices hvis tilgængelig
const materialPrice = value.price ? parseFloat(value.price) : 0;
setMaterialData({
...materialData,
material_id: value.id,
material_name: value.name,
description: value.description || '',
unit: value.unit || 'stk',
price: value.price || 0
price: materialPrice
});
// Fjern material_id fejl
@@ -266,7 +269,10 @@ const MaterialDialog = ({ open, onClose, onSave, material, availableMaterials })
<Box flex={1}>
<Autocomplete
options={availableMaterials || []}
getOptionLabel={(option) => `${option.name} (${option.varenr || 'Uden varenr'})`}
getOptionLabel={(option) => {
const priceInfo = option.price ? ` - ${parseFloat(option.price).toFixed(2)} kr/${option.unit}` : '';
return `${option.name}${priceInfo}`;
}}
value={selectedMaterial}
onChange={handleMaterialSelect}
renderInput={(params) => (

View File

@@ -1,8 +1,8 @@
{
"last_sync": "2025-10-24T07:02:59+00:00",
"last_sync": "2025-10-25T04:02:59+00:00",
"status": "success",
"duration_seconds": 177,
"total_records": 68723,
"duration_seconds": 178,
"total_records": 68746,
"active_employees": 19,
"calendar_entries_next_7_days": 10
"calendar_entries_next_7_days": 7
}

112
test-markup-sliders.js Normal file
View File

@@ -0,0 +1,112 @@
/**
* Test af markup sliders for SmartPackages
* - Material markup slider (standard 20%, justerbar 0-50%)
* - Final review markup slider (standard 2%, justerbar 0-10%)
*/
const axios = require('axios');
const API_URL = 'http://localhost:4031/api/smart-packages';
// Test geometri
const testGeometry = {
length_main: 15, // Facadelængde
width_main: 10, // Husbredde
total_area: 85, // Tagflade m²
nedløb_antal: 4
};
async function testMarkupScenarios() {
console.log('🧪 Test af Markup Sliders for SmartPackages\n');
console.log('📐 Test geometri:');
console.log(` Facadelængde: ${testGeometry.length_main}m`);
console.log(` Husbredde: ${testGeometry.width_main}m`);
console.log(` Tagflade: ${testGeometry.total_area}\n`);
// Test forskellige markup kombinationer
const scenarios = [
{
name: 'Standard (20% material + 2% final)',
materialMarkup: 20,
finalMarkup: 2
},
{
name: 'Lav margin (10% material + 1% final)',
materialMarkup: 10,
finalMarkup: 1
},
{
name: 'Høj margin (30% material + 5% final)',
materialMarkup: 30,
finalMarkup: 5
},
{
name: 'Ingen final markup (20% material + 0% final)',
materialMarkup: 20,
finalMarkup: 0
}
];
for (const scenario of scenarios) {
console.log('═══════════════════════════════════════════════════════════');
console.log(`📊 ${scenario.name}`);
console.log('═══════════════════════════════════════════════════════════\n');
try {
const response = await axios.post(`${API_URL}/97/calculate-with-geometry`, {
geometry: testGeometry,
materialMarkupPercent: scenario.materialMarkup,
finalReviewMarkupPercent: scenario.finalMarkup
});
const data = response.data;
console.log(`📦 Pakke: ${data.package.name}`);
console.log(`\n💰 Markup indstillinger:`);
console.log(` Material markup: ${scenario.materialMarkup}%`);
console.log(` Final review markup: ${scenario.finalMarkup}%`);
console.log(`\n📊 Beregning:`);
console.log(` Materialer (inkl. ${scenario.materialMarkup}% markup): ${data.totals.material_cost.toFixed(2)} kr`);
console.log(` Arbejdsløn: ${data.totals.labor_cost.toFixed(2)} kr`);
console.log(` ────────────────────────────────`);
console.log(` Subtotal: ${data.totals.subtotal.toFixed(2)} kr`);
console.log(` Final review markup (${scenario.finalMarkup}%): +${data.totals.final_review_markup.toFixed(2)} kr`);
console.log(` ════════════════════════════════`);
console.log(` TOTAL: ${data.totals.total_cost.toFixed(2)} kr\n`);
// Vis et par eksempel materialer
if (data.materials.length > 0) {
console.log(`📦 Eksempel materialer (${scenario.materialMarkup}% markup):`);
data.materials.slice(0, 3).forEach(m => {
console.log(`${m.name}`);
console.log(` Antal: ${m.quantity} stk`);
if (m.base_price > 0) {
console.log(` Indkøbspris: ${m.base_price.toFixed(2)} kr/stk`);
console.log(` Salgspris: ${m.price.toFixed(2)} kr/stk (inkl. ${m.markup_percent}% markup)`);
console.log(` Total: ${(m.quantity * m.price).toFixed(2)} kr`);
} else {
console.log(` Pris: Ikke importeret endnu`);
}
});
}
console.log('\n');
} catch (error) {
console.error(`❌ Fejl i scenario "${scenario.name}":`, error.response?.data || error.message);
console.log('\n');
}
}
console.log('═══════════════════════════════════════════════════════════');
console.log('✅ Test gennemført!');
console.log('═══════════════════════════════════════════════════════════');
console.log('\n💡 Næste skridt:');
console.log(' 1. Tilføj markup sliders i frontend UI');
console.log(' 2. Gem markup værdier i quotes tabellen');
console.log(' 3. Import materialer fra Bygma med korrekte priser');
console.log(' 4. Vis final review markup slider i review trin\n');
}
testMarkupScenarios().catch(console.error);

104
test-package-calculation.js Normal file
View File

@@ -0,0 +1,104 @@
/**
* Test script til at teste SmartPackage geometri-beregninger
*/
const axios = require('axios');
const mysql = require('mysql2/promise');
const LOCAL_API = 'http://localhost:4031';
const dbConfig = {
host: 'localhost',
user: 'tilbudgivern_service',
password: 'REDACTED_PASSWORD',
database: 'tilbudgivern'
};
async function testPackageCalculation() {
console.log('🧪 Tester SmartPackage geometri-beregning...\n');
try {
// Test geometri (eksempel hus)
const testGeometry = {
length_main: 15, // 15 meter facadelængde
width_main: 10, // 10 meter husbredde
total_area: 85, // 85 m² tagflade
stern_to_ridge_height: 3.5,
roof_angle_degrees: 30,
nedløb_antal: 4
};
console.log('📐 Test geometri:');
console.log(` Facadelængde: ${testGeometry.length_main}m`);
console.log(` Husbredde: ${testGeometry.width_main}m`);
console.log(` Tagflade: ${testGeometry.total_area}`);
console.log(` Nedløb antal: ${testGeometry.nedløb_antal}\n`);
// Test hver pakke
const packageIds = [97, 98, 99, 100, 101, 102]; // Tagrende, Nedløb, Spær, B7, Rygning, Taghætte
const packageNames = [
'Tagrende med Konsoljern',
'Nedløb komplet',
'Opretning af spær',
'B7 montering inkl. lægter',
'Rygning B7',
'Taghætte B7'
];
for (let i = 0; i < packageIds.length; i++) {
const packageId = packageIds[i];
const packageName = packageNames[i];
console.log(`\n${'='.repeat(60)}`);
console.log(`📦 ${packageName} (ID: ${packageId})`);
console.log('='.repeat(60));
try {
const response = await axios.post(
`${LOCAL_API}/api/smart-packages/${packageId}/calculate-with-geometry`,
{ geometry: testGeometry }
);
if (response.data.success) {
const result = response.data;
console.log(`\n✅ Beregning gennemført!`);
console.log(`\n📊 Materialer (${result.materials.length} stk):`);
result.materials.forEach((m, idx) => {
console.log(` ${idx + 1}. ${m.name}`);
console.log(` Antal: ${m.quantity}`);
console.log(` Pris: ${m.price.toFixed(2)} kr (inkl. 20% avance)`);
console.log(` Formel: ${m.formula_used}`);
console.log(` Total: ${(m.quantity * m.price).toFixed(2)} kr\n`);
});
console.log(`\n⏱️ Arbejde:`);
console.log(` Timer: ${result.labor.hours.toFixed(2)}`);
console.log(` Timepris: ${result.labor.hourly_rate} kr`);
console.log(` Arbejdsløn: ${result.labor.cost.toFixed(2)} kr`);
console.log(`\n💰 Totaler:`);
console.log(` Materialer: ${result.totals.material_cost.toFixed(2)} kr`);
console.log(` Arbejde: ${result.totals.labor_cost.toFixed(2)} kr`);
console.log(` ────────────────────────────────`);
console.log(` TOTAL: ${result.totals.total_cost.toFixed(2)} kr`);
} else {
console.log(`❌ Fejl: ${response.data.error}`);
}
} catch (error) {
console.log(`❌ Kunne ikke beregne pakke ${packageId}:`, error.response?.data?.error || error.message);
}
}
console.log(`\n${'='.repeat(60)}`);
console.log('✅ Test gennemført!');
console.log('='.repeat(60));
} catch (error) {
console.error('\n❌ Test fejlede:', error.message);
}
}
// Kør test
testPackageCalculation();

View File

@@ -0,0 +1,89 @@
/**
* Test at stern_til_kip variabel kan bruges i SmartPackage formler
*/
const axios = require('axios');
const API_URL = 'http://localhost:4031/api/smart-packages';
// Test geometri MED stern_to_ridge_height
const testGeometry = {
length_main: 15, // Facadelængde
width_main: 10, // Husbredde
total_area: 85, // Tagflade m²
nedløb_antal: 4,
stern_to_ridge_height: 3.5, // Højde stern til kip (NYT!)
roof_height: 2.8 // Beregnet højde til ryg
};
async function testSternTilKipFormula() {
console.log('🧪 Test: stern_til_kip variabel i SmartPackage formler\n');
console.log('📐 Test geometri:');
console.log(` Facadelængde: ${testGeometry.length_main}m`);
console.log(` Husbredde: ${testGeometry.width_main}m`);
console.log(` Tagflade: ${testGeometry.total_area}`);
console.log(` Højde stern til kip: ${testGeometry.stern_to_ridge_height}m 🆕`);
console.log(` Beregnet højde til ryg: ${testGeometry.roof_height}m\n`);
try {
// Test Tagrende pakke (bruger facadelængde)
console.log('═══════════════════════════════════════════════════════════');
console.log('📦 Test 1: Tagrende pakke (facadelængde formel)');
console.log('═══════════════════════════════════════════════════════════\n');
const response1 = await axios.post(`${API_URL}/97/calculate-with-geometry`, {
geometry: testGeometry,
materialMarkupPercent: 20,
finalReviewMarkupPercent: 2
});
const data1 = response1.data;
console.log(`${data1.package.name}`);
console.log(`\n📊 Geometri brugt i beregning:`);
console.log(` Facadelængde: ${data1.calculation_info.geometry_used.facadelængde}m`);
console.log(` Husbredde: ${data1.calculation_info.geometry_used.husbredde}m`);
console.log(` Tagflade: ${data1.calculation_info.geometry_used.tagflade_m2}`);
// Vis materiale der bruger facadelængde
const tagrende = data1.materials.find(m => m.name.includes('Tagrende'));
if (tagrende) {
console.log(`\n📦 Eksempel materiale med formel:`);
console.log(` ${tagrende.name}`);
console.log(` Formel: ${tagrende.formula_used}`);
console.log(` Beregnet antal: ${tagrende.quantity} stk`);
console.log(` Forventet: ${Math.ceil(testGeometry.length_main / 3)} stk (15m ÷ 3)`);
}
console.log('\n');
// Test med en hypotetisk formel der bruger stern_til_kip
console.log('═══════════════════════════════════════════════════════════');
console.log('📦 Test 2: Verificer stern_til_kip er tilgængelig');
console.log('═══════════════════════════════════════════════════════════\n');
console.log('💡 Tilgængelige geometri variabler i formler:');
console.log(' ✅ facadelængde → length_main');
console.log(' ✅ husbredde → width_main');
console.log(' ✅ tagflade_m2 → total_area');
console.log(' ✅ nedløb_antal → nedløb_antal');
console.log(' ✅ stern_til_kip → stern_to_ridge_height 🆕');
console.log(' ✅ højde_stern_til_kip → stern_to_ridge_height 🆕');
console.log(' ✅ taghøjde → roof_height\n');
console.log('📝 Eksempel formler der nu kan bruges:');
console.log(' • "stern_til_kip × 2" → 3.5 × 2 = 7');
console.log(' • "facadelængde + stern_til_kip" → 15 + 3.5 = 18.5');
console.log(' • "højde_stern_til_kip ÷ 0.5" → 3.5 ÷ 0.5 = 7\n');
console.log('═══════════════════════════════════════════════════════════');
console.log('✅ Test gennemført!');
console.log('═══════════════════════════════════════════════════════════');
console.log('\n💡 Højde stern til kip er nu gemt korrekt i database');
console.log(' og kan bruges i SmartPackage formler!\n');
} catch (error) {
console.error('❌ Fejl:', error.response?.data || error.message);
}
}
testSternTilKipFormula().catch(console.error);

View File

@@ -0,0 +1,90 @@
/**
* Test at Højde stern til kip (wallHeight) gemmes korrekt i databasen
*/
const axios = require('axios');
const API_URL = 'http://localhost:4031/api/customer-projects';
async function testSternToRidgeHeight() {
console.log('🧪 Test: Gem Højde stern til kip\n');
try {
// Find et eksisterende projekt
const projectsResponse = await axios.get(`${API_URL}/projects`);
const projects = projectsResponse.data.projects;
if (projects.length === 0) {
console.log('❌ Ingen projekter fundet. Opret et projekt først.');
return;
}
const testProject = projects[0];
console.log(`📋 Bruger projekt ID: ${testProject.id}`);
console.log(` Kunde: ${testProject.customer_name}\n`);
// Test data med wallHeight (Højde stern til kip)
const testGeometry = {
roofType: 'skraat_tag', // ENUM værdi fra database
roofWidth: 10,
roofLength: 15,
roofPitch: 30,
wallHeight: 3.5, // DETTE ER NØGLEN - Højde stern til kip
ridgeHeight: 2.8, // Beregnet højde til ryg
complexity: 1.0,
notes: 'Test af stern til kip højde'
};
console.log('📤 Gemmer geometri med:');
console.log(` Højde stern til kip (wallHeight): ${testGeometry.wallHeight}m`);
console.log(` Beregnet højde til ryg: ${testGeometry.ridgeHeight}m\n`);
// Gem geometri
const saveResponse = await axios.post(
`${API_URL}/projects/${testProject.id}/geometry`,
testGeometry
);
if (saveResponse.data.success) {
console.log('✅ Geometri gemt succesfuldt!\n');
// Hent geometri tilbage for at verificere
const getResponse = await axios.get(`${API_URL}/projects/${testProject.id}/geometry`);
const savedGeometry = getResponse.data.geometry;
console.log('📥 Hentet geometri fra database:');
console.log(` stern_to_ridge_height: ${savedGeometry.stern_to_ridge_height}m`);
console.log(` roof_height: ${savedGeometry.roof_height}m`);
console.log(` width_main: ${savedGeometry.width_main}m`);
console.log(` length_main: ${savedGeometry.length_main}m\n`);
// Verificér at stern_to_ridge_height er gemt korrekt
if (parseFloat(savedGeometry.stern_to_ridge_height) === testGeometry.wallHeight) {
console.log('✅ SUCCESS: Højde stern til kip gemt korrekt!');
console.log(` Forventet: ${testGeometry.wallHeight}m`);
console.log(` Gemt: ${savedGeometry.stern_to_ridge_height}m\n`);
} else {
console.log('❌ FEJL: Højde stern til kip IKKE gemt korrekt!');
console.log(` Forventet: ${testGeometry.wallHeight}m`);
console.log(` Gemt: ${savedGeometry.stern_to_ridge_height}m`);
console.log(` Difference: ${Math.abs(parseFloat(savedGeometry.stern_to_ridge_height) - testGeometry.wallHeight)}m\n`);
}
// Vis hvad der kan bruges i SmartPackage beregninger
console.log('💡 Tilgængelige geometri værdier for SmartPackages:');
console.log(` ✅ length_main (facadelængde): ${savedGeometry.length_main}m`);
console.log(` ✅ width_main (husbredde): ${savedGeometry.width_main}m`);
console.log(` ✅ total_area (tagflade): ${savedGeometry.total_area}`);
console.log(` ✅ stern_to_ridge_height (højde stern til kip): ${savedGeometry.stern_to_ridge_height}m`);
console.log(` ✅ roof_height (beregnet højde til ryg): ${savedGeometry.roof_height}m\n`);
} else {
console.log('❌ Fejl ved gem af geometri:', saveResponse.data.error);
}
} catch (error) {
console.error('❌ Fejl:', error.response?.data || error.message);
}
}
testSternToRidgeHeight().catch(console.error);