const logger = require('../utils/logger'); class ProjectMaterialService { constructor(databaseService) { this.db = databaseService; } // Tilføj materiale til projekt async addProjectMaterial(projectId, materialData) { try { const { materialName, materialCategory, quantity, unit, unitPrice, supplier, materialSource = 'manual', notes } = materialData; const totalPrice = quantity * unitPrice; const query = ` INSERT INTO project_materials ( project_id, material_name, material_category, quantity, unit, unit_price, total_price, supplier, material_source, notes ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; const [result] = await this.db.pool.execute(query, [ projectId, materialName, materialCategory, quantity, unit, unitPrice, totalPrice, supplier, materialSource, notes ]); logger.info('Project material added', { projectId, materialName, quantity, totalPrice }); return { id: result.insertId, materialName, materialCategory, quantity, unit, unitPrice, totalPrice, supplier, materialSource }; } catch (error) { logger.error('Error adding project material:', error); throw error; } } // Hent alle materialer for projekt async getProjectMaterials(projectId) { try { const [rows] = await this.db.pool.execute( `SELECT * FROM project_materials WHERE project_id = ? ORDER BY material_category, material_name`, [projectId] ); // Gruppér materialer per kategori const materialsByCategory = {}; let totalMaterialCost = 0; rows.forEach(material => { const category = material.material_category || 'Øvrige'; if (!materialsByCategory[category]) { materialsByCategory[category] = []; } materialsByCategory[category].push(material); totalMaterialCost += parseFloat(material.total_price); }); return { materials: rows, materialsByCategory, totalMaterialCost, materialCount: rows.length }; } catch (error) { logger.error('Error getting project materials:', error); throw error; } } // Opdater materiale async updateProjectMaterial(materialId, updateData) { try { const { materialName, materialCategory, quantity, unit, unitPrice, supplier, notes } = updateData; const totalPrice = quantity * unitPrice; const query = ` UPDATE project_materials SET material_name = ?, material_category = ?, quantity = ?, unit = ?, unit_price = ?, total_price = ?, supplier = ?, notes = ? WHERE id = ? `; const [result] = await this.db.pool.execute(query, [ materialName, materialCategory, quantity, unit, unitPrice, totalPrice, supplier, notes, materialId ]); if (result.affectedRows === 0) { throw new Error('Material not found'); } logger.info('Project material updated', { materialId, materialName, quantity, totalPrice }); return { id: materialId, materialName, materialCategory, quantity, unit, unitPrice, totalPrice, supplier }; } catch (error) { logger.error('Error updating project material:', error); throw error; } } // Slet materiale async deleteProjectMaterial(materialId) { try { const [result] = await this.db.pool.execute( 'DELETE FROM project_materials WHERE id = ?', [materialId] ); if (result.affectedRows === 0) { throw new Error('Material not found'); } logger.info('Project material deleted', { materialId }); return true; } catch (error) { logger.error('Error deleting project material:', error); throw error; } } // Importer materialer fra eksisterende database async importMaterialsFromDatabase(projectId, searchCriteria) { try { const { category, searchTerm, limit = 50 } = searchCriteria; let query = ` SELECT DISTINCT name, material_category, unit, unit_price, supplier_name FROM ocr_materials WHERE 1=1 `; let params = []; if (category) { query += ' AND material_category = ?'; params.push(category); } if (searchTerm) { query += ' AND (name LIKE ? OR supplier_name LIKE ?)'; params.push(`%${searchTerm}%`, `%${searchTerm}%`); } query += ' ORDER BY name LIMIT ?'; params.push(limit); const [rows] = await this.db.pool.execute(query, params); logger.info('Materials found in database', { projectId, foundCount: rows.length, searchCriteria }); return rows.map(row => ({ materialName: row.name, materialCategory: row.material_category, unit: row.unit, unitPrice: row.unit_price, supplier: row.supplier_name, materialSource: 'database' })); } catch (error) { logger.error('Error importing materials from database:', error); throw error; } } // Bulk tilføj materialer async bulkAddMaterials(projectId, materials) { try { const addedMaterials = []; let totalCost = 0; for (const material of materials) { const result = await this.addProjectMaterial(projectId, material); addedMaterials.push(result); totalCost += result.totalPrice; } logger.info('Bulk materials added', { projectId, materialCount: addedMaterials.length, totalCost }); return { addedMaterials, totalCost, count: addedMaterials.length }; } catch (error) { logger.error('Error bulk adding materials:', error); throw error; } } // Hent material kategorier fra database async getMaterialCategories() { try { const [rows] = await this.db.pool.execute(` SELECT DISTINCT material_category as category, COUNT(*) as count FROM ocr_materials WHERE material_category IS NOT NULL AND material_category != '' GROUP BY material_category ORDER BY count DESC, material_category ASC `); return rows; } catch (error) { logger.error('Error getting material categories:', error); throw error; } } // Beregn materialeomkostninger for projekt async calculateMaterialCosts(projectId) { try { const [rows] = await this.db.pool.execute(` SELECT SUM(total_price) as total_cost, COUNT(*) as material_count, material_category, SUM(total_price) as category_cost FROM project_materials WHERE project_id = ? GROUP BY material_category `, [projectId]); const [totalRows] = await this.db.pool.execute(` SELECT SUM(total_price) as total_cost, COUNT(*) as material_count FROM project_materials WHERE project_id = ? `, [projectId]); const categoryBreakdown = rows.map(row => ({ category: row.material_category || 'Øvrige', cost: parseFloat(row.category_cost), materialCount: parseInt(row.material_count) })); return { totalMaterialCost: totalRows[0]?.total_cost ? parseFloat(totalRows[0].total_cost) : 0, materialCount: totalRows[0]?.material_count ? parseInt(totalRows[0].material_count) : 0, categoryBreakdown }; } catch (error) { logger.error('Error calculating material costs:', error); throw error; } } // Tag materiale forslag baseret på tag type og areal async suggestRoofMaterials(projectId) { try { // Hent geometri data const [geometryRows] = await this.db.pool.execute( 'SELECT * FROM roof_geometry WHERE project_id = ?', [projectId] ); if (geometryRows.length === 0) { throw new Error('No geometry data found for project'); } const geometry = geometryRows[0]; const suggestions = []; // Hent rigtige materialer fra database const [roofMaterials] = await this.db.pool.execute(` SELECT name, category, price, unit FROM material_prices WHERE category IN ('Tagmateriale', 'Tag', 'Tagmaterialer', 'Tagpap', 'Tagplader') ORDER BY price ASC LIMIT 10 `); const [woodMaterials] = await this.db.pool.execute(` SELECT name, category, price, unit FROM material_prices WHERE category IN ('Træ', 'Trælast', 'Konstruktionstræ') ORDER BY price ASC LIMIT 8 `); const [insulationMaterials] = await this.db.pool.execute(` SELECT name, category, price, unit FROM material_prices WHERE category IN ('Isolering', 'Isolation') ORDER BY price ASC LIMIT 5 `); const [fastenerMaterials] = await this.db.pool.execute(` SELECT name, category, price, unit FROM material_prices WHERE category IN ('Beslag', 'Befæstning', 'Skruer') ORDER BY price ASC LIMIT 6 `); // Basis materialer for tag arbejde - nu med database integration const roofMaterialSuggestions = { 'fladt_tag': [ { name: 'EPDM tagfolie', unit: 'm²', estimatedQuantity: geometry.total_area * 1.1, price: 165.00 }, { name: 'Tagpap', unit: 'm²', estimatedQuantity: geometry.total_area * 1.1, price: 85.00 }, { name: 'Isolering EPS', unit: 'm²', estimatedQuantity: geometry.total_area, price: 95.00 }, { name: 'Træbeton', unit: 'm²', estimatedQuantity: geometry.total_area, price: 125.00 }, { name: 'Tagrender', unit: 'løbende m', estimatedQuantity: geometry.perimeter, price: 185.00 }, { name: 'Tætningslister', unit: 'løbende m', estimatedQuantity: geometry.perimeter * 1.1, price: 15.00 } ], 'skraat_tag': [ { name: 'Tagsten', unit: 'm²', estimatedQuantity: geometry.total_area * 1.15, price: 195.00 }, { name: 'Undertag', unit: 'm²', estimatedQuantity: geometry.total_area, price: 65.00 }, { name: 'Tagcentraler', unit: 'stk', estimatedQuantity: Math.ceil(geometry.total_area / 10), price: 85.00 }, { name: 'Mineraluld 200mm', unit: 'm²', estimatedQuantity: geometry.total_area, price: 135.00 }, { name: 'Spær 45x195mm', unit: 'stk', estimatedQuantity: Math.ceil(geometry.length / 0.6), price: 125.00 }, { name: 'Lægte 38x63mm', unit: 'løbende m', estimatedQuantity: geometry.total_area * 2.5, price: 18.50 }, { name: 'Vindskeder', unit: 'løbende m', estimatedQuantity: geometry.perimeter, price: 95.00 }, { name: 'Tagrender Ø150mm', unit: 'løbende m', estimatedQuantity: geometry.length * 2, price: 185.00 }, { name: 'Nedløb Ø100mm', unit: 'stk', estimatedQuantity: Math.max(2, Math.ceil(geometry.length / 8)), price: 125.00 } ], 'mansard': [ { name: 'Tagsten premium', unit: 'm²', estimatedQuantity: geometry.total_area * 1.2, price: 245.00 }, { name: 'Undertag diffusionsåben', unit: 'm²', estimatedQuantity: geometry.total_area, price: 75.00 }, { name: 'Tagcentraler forstærket', unit: 'stk', estimatedQuantity: Math.ceil(geometry.total_area / 8), price: 95.00 }, { name: 'Mineraluld 250mm', unit: 'm²', estimatedQuantity: geometry.total_area, price: 165.00 }, { name: 'Spær forstærket', unit: 'stk', estimatedQuantity: Math.ceil(geometry.length / 0.5), price: 145.00 }, { name: 'Specialbeslag', unit: 'sæt', estimatedQuantity: Math.ceil(geometry.total_area / 15), price: 185.00 } ], 'komplekst': [ { name: 'Premium tagsten', unit: 'm²', estimatedQuantity: geometry.total_area * 1.25, price: 295.00 }, { name: 'Højkvalitets undertag', unit: 'm²', estimatedQuantity: geometry.total_area, price: 95.00 }, { name: 'Specialbeslag', unit: 'sæt', estimatedQuantity: Math.ceil(geometry.total_area / 20), price: 245.00 }, { name: 'Mineraluld premium', unit: 'm²', estimatedQuantity: geometry.total_area, price: 195.00 }, { name: 'Ventilationssystem', unit: 'sæt', estimatedQuantity: Math.ceil(geometry.total_area / 50), price: 385.00 } ] }; let materialList = roofMaterialSuggestions[geometry.roof_type] || roofMaterialSuggestions['skraat_tag']; // Tilføj database materialer hvis tilgængelige if (roofMaterials.length > 0) { roofMaterials.forEach(material => { materialList.push({ name: material.name, unit: material.unit, estimatedQuantity: geometry.total_area * 1.1, price: parseFloat(material.price), category: 'Tagmateriale' }); }); } if (woodMaterials.length > 0) { woodMaterials.forEach(material => { materialList.push({ name: material.name, unit: material.unit, estimatedQuantity: material.unit.includes('m²') ? geometry.total_area : geometry.length * 2, price: parseFloat(material.price), category: 'Træ' }); }); } if (insulationMaterials.length > 0) { insulationMaterials.forEach(material => { materialList.push({ name: material.name, unit: material.unit, estimatedQuantity: geometry.total_area, price: parseFloat(material.price), category: 'Isolering' }); }); } if (fastenerMaterials.length > 0) { fastenerMaterials.forEach(material => { materialList.push({ name: material.name, unit: material.unit, estimatedQuantity: material.unit === 'pakke' ? 1 : Math.ceil(geometry.total_area * 6), price: parseFloat(material.price), category: 'Beslag' }); }); } // Tilføj ekstra materialer for specielle forhold if (geometry.has_dormers) { materialList.push({ name: 'Kvist materialer ekstra', unit: 'sæt', estimatedQuantity: geometry.dormer_count || 1, price: 485.00, category: 'Tagmaterialer' }); materialList.push({ name: 'Kvist blinklister', unit: 'løbende m', estimatedQuantity: (geometry.dormer_count || 1) * 8, price: 45.00, category: 'Tætning' }); } if (geometry.has_skylights) { materialList.push({ name: 'Ovenlys inddækning', unit: 'stk', estimatedQuantity: 2, price: 185.00, category: 'Specialmaterialer' }); } // Tilføj standard tilbehør baseret på projekt størrelse if (geometry.total_area > 100) { materialList.push({ name: 'Tagstiger galvaniseret', unit: 'stk', estimatedQuantity: 1, price: 850.00, category: 'Tilbehør' }); } if (geometry.total_area > 50) { materialList.push({ name: 'Snefang system', unit: 'løbende m', estimatedQuantity: geometry.length, price: 75.00, category: 'Tilbehør' }); } // Konverter til suggestions format med forbedret kategorisering materialList.forEach(material => { suggestions.push({ name: material.name, unit: material.unit, estimatedQuantity: Math.ceil(material.estimatedQuantity), unitPrice: material.price, category: material.category || 'Tagmaterialer', calculation: `${material.estimatedQuantity.toFixed(1)} ${material.unit} baseret på ${geometry.total_area}m² tagareal`, source: 'database_enhanced' }); }); return suggestions; } catch (error) { logger.error('Error suggesting roof materials:', error); throw error; } } } module.exports = ProjectMaterialService;