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:
275
MARKUP_SLIDER_SYSTEM.md
Normal file
275
MARKUP_SLIDER_SYSTEM.md
Normal 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 på 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 på 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
517
create-experience-based-packages.js
Normal file
517
create-experience-based-packages.js
Normal 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
265
create-packages-simple.js
Normal 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
171
create-packages-v3.js
Normal 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();
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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
112
test-markup-sliders.js
Normal 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}m²\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
104
test-package-calculation.js
Normal 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}m²`);
|
||||
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();
|
||||
89
test-stern-til-kip-formula.js
Normal file
89
test-stern-til-kip-formula.js
Normal 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}m²`);
|
||||
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}m²`);
|
||||
|
||||
// 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);
|
||||
90
test-stern-to-ridge-height.js
Normal file
90
test-stern-to-ridge-height.js
Normal 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}m²`);
|
||||
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);
|
||||
Reference in New Issue
Block a user