Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | const fs = require('fs'); const path = require('path'); class QuoteTemplateService { constructor() { this.templatePath = path.join(__dirname, '../../../backend/templates/quote-template.html'); this.logoPath = path.join(__dirname, '../../../frontend/firmadata/firmabilleder'); } // Convert image to base64 async imageToBase64(imagePath) { try { const imageBuffer = fs.readFileSync(imagePath); return imageBuffer.toString('base64'); } catch (error) { console.error('Error converting image to base64:', error); return ''; } } // Load and encode all logos async loadLogos() { const logos = { mikhaelLogo: await this.imageToBase64(path.join(this.logoPath, 'MIKHOL_logo.png')), bygGarantiLogo: await this.imageToBase64(path.join(this.logoPath, 'byggarenti_logo.png')), eliteLogo: await this.imageToBase64(path.join(this.logoPath, 'elitehaandvaerker_logo.png')) }; return logos; } // Generate work description based on materials and geometry generateWorkDescription(data) { const { project, geometry, materials } = data; let description = []; // Standard start description.push('Ansøgning om anvisningsplads for asbest affald udføres.'); description.push('Stillads opstilles til opgaven'); // Material-based descriptions if (materials.some(m => m.material_name?.toLowerCase().includes('tagplade'))) { description.push('Eksisterende B7 plader demonteres og håndteres med teleskoplæsser'); } if (materials.some(m => m.material_name?.toLowerCase().includes('lægte'))) { description.push('Lægter demonteres, og nye godkendte T1 taglægter monteres.'); } description.push('Tagflader afdækkes med presenninger i perioden inden nye plader monteres.'); if (geometry?.roof_type === 'skraat_tag') { description.push('Inddækninger til skorsten demonteres og nye monteres efter forskrifter.'); description.push('Ventileret tagfod og rygning udføres.'); } if (materials.some(m => m.material_name?.toLowerCase().includes('rende'))) { description.push('Tagrender udskiftes til nye Plastmo aluzink 11" tagrender og nye rendejern monteres samt 2 nedløb.'); } if (materials.some(m => m.material_name?.toLowerCase().includes('tagplade'))) { description.push('Nye sorte B7 sort/blå Cembrit tagplader leveres og monteres efter forskrifter.'); description.push('Rygnings plader leveres og monteres som ventileret med DE flex profiler.'); } if (materials.some(m => m.material_name?.toLowerCase().includes('taghætte'))) { description.push('Taghætter udskiftes til nye Cembrit 3 stk.'); } description.push('sternbrædder og vindskeder skiftes til nye sortmalede i samme dimension som eksisterende'); return description.join('\n'); } // Generate HTML quote async generateHtmlQuote(data) { try { // Load template const template = fs.readFileSync(this.templatePath, 'utf8'); // Load logos const logos = await this.loadLogos(); // Prepare data const { project, geometry, labor, materials, totals } = data; const quoteNumber = `${Date.now().toString().slice(-4)}`; const currentDate = new Date().toLocaleDateString('da-DK'); // Company information const COMPANY_INFO = { name: "Tømrer- og Snedkermester Mikael Holck ApS", phone: "42 46 81 10", cvr: "38178059", address: "Hornumvej 7", city: "4600 Køge", bank: "Nordea", email: "mikael@holck.dk", account: "2558 6284200630", website: "www.mikaelholck.dk" }; // Format materials and labor items const items = []; // Add labor if (labor && labor.length > 0) { const workDescription = geometry?.roof_type === 'skraat_tag' ? 'Tag m²' : 'Arbejdsløn'; items.push({ description: workDescription, amount: totals.laborTotal.toLocaleString('da-DK', {minimumFractionDigits: 2, maximumFractionDigits: 2}) }); } // Add materials materials.forEach(m => { const amount = (parseFloat(m.unit_price) * parseFloat(m.quantity)); items.push({ description: m.material_name, amount: amount.toLocaleString('da-DK', {minimumFractionDigits: 2, maximumFractionDigits: 2}) }); }); // Template variables const templateVars = { // Logos ...logos, // Quote info quoteNumber, quoteDate: currentDate, customerNumber: project.id, // Customer info customerName: project.customer_name, customerAddress: project.customer_email || '', // Project info projectAddress: project.project_address || `${project.customer_name} adresse`, projectTitle: project.project_name, workDescription: this.generateWorkDescription(data), // Items - convert to simple replacement itemsHtml: items.map(item => `<tr><td>${item.description}</td><td>${item.amount}</td></tr>` ).join(''), // Totals subtotal: totals.subtotal.toLocaleString('da-DK', {minimumFractionDigits: 2, maximumFractionDigits: 2}), vat: totals.vat.toLocaleString('da-DK', {minimumFractionDigits: 2, maximumFractionDigits: 2}), total: totals.total.toLocaleString('da-DK', {minimumFractionDigits: 2, maximumFractionDigits: 2}), // Company info companyName: COMPANY_INFO.name, companyAddress: COMPANY_INFO.address, companyCity: COMPANY_INFO.city, companyPhone: COMPANY_INFO.phone, companyCvr: COMPANY_INFO.cvr, companyBank: COMPANY_INFO.bank, companyEmail: COMPANY_INFO.email, companyAccount: COMPANY_INFO.account, companyWebsite: COMPANY_INFO.website }; // Simple template replacement (since we don't have handlebars) let html = template; // Replace all template variables Object.keys(templateVars).forEach(key => { const regex = new RegExp(`{{${key}}}`, 'g'); html = html.replace(regex, templateVars[key] || ''); }); // Handle items loop manually const itemsSection = items.map(item => ` <tr> <td>${item.description}</td> <td>${item.amount}</td> </tr>` ).join('\n'); html = html.replace(' {{#each items}}\n <tr>\n <td>{{this.description}}</td>\n <td>{{this.amount}}</td>\n </tr>\n {{/each}}', itemsSection); return html; } catch (error) { console.error('Error generating HTML quote:', error); throw error; } } } module.exports = QuoteTemplateService; |