Error executing template "/Designs/Swift/Paragraph/Custom_Offer.cshtml"
System.NullReferenceException: Object reference not set to an instance of an object.
   at CompiledRazorTemplates.Dynamic.RazorEngine_2261fb47494b4fada1d26dfbefa12a29.ExecuteAsync()
   at RazorEngine.Templating.TemplateBase.Run(ExecuteContext context, TextWriter reader)
   at RazorEngine.Templating.RazorEngineCore.RunTemplate(ICompiledTemplate template, TextWriter writer, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.DynamicWrapperService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass23_0.<Run>b__0(TextWriter writer)
   at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter)
   at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, Type modelType, Object model, DynamicViewBag viewBag)
   at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template)
   at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template)
   at Dynamicweb.Rendering.Template.RenderRazorTemplate()

1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel> 2 @using Dynamicweb.Security.UserManagement; 3 @using System 4 @using System.Web 5 @using System.Globalization 6 @using System.Linq 7 @using Dynamicweb.Ecommerce.ProductCatalog 8 @using Dynamicweb.Rendering 9 @using Dynamicweb.Ecommerce.Products 10 11 @{ 12 var productsParam = Dynamicweb.Context.Current.Request.QueryString["products"]; 13 var productLines = !string.IsNullOrEmpty(productsParam) ? productsParam.Split(',') : new string[0]; 14 15 var products = productLines 16 .Select(line => 17 { 18 var s = (line ?? "").Trim(); 19 if (string.IsNullOrEmpty(s)) 20 return new { Number = "", VariantId = "", Qty = 1 }; 21 22 // Split på ~ (qty-separator) 23 var partsQty = s.Split('~'); 24 var idPart = partsQty.Length > 0 ? partsQty[0].Trim() : ""; 25 var qty = 1; 26 27 if (partsQty.Length > 1) 28 { 29 int.TryParse(partsQty[1], out qty); 30 if (qty < 1) qty = 1; 31 } 32 33 // Split NUMBER|VARIANTID 34 var nv = idPart.Split('|'); 35 var number = nv.Length > 0 ? nv[0].Trim() : ""; 36 var variantId = nv.Length > 1 ? nv[1].Trim() : ""; 37 38 return new { Number = number, VariantId = variantId, Qty = qty }; 39 }) 40 .Where(x => !string.IsNullOrEmpty(x.Number)) 41 .ToList(); 42 43 string title = Model.Item.GetString("Title"); 44 string description = Model.Item.GetString("Description"); 45 46 var currentUser = Dynamicweb.Security.UserManagement.User.GetCurrentExtranetUser(); 47 var secondaryUser = Dynamicweb.Security.UserManagement.User.GetCurrentSecondaryUser(); 48 49 var salesPersonName = secondaryUser != null ? secondaryUser.Name : currentUser?.Name; 50 var salesPersonMail = secondaryUser != null ? secondaryUser.Email : currentUser?.Email; 51 52 var customerAddresses = currentUser.GetAddresses(); 53 } 54 55 <style> 56 .qty-wrapper { 57 display: flex; 58 align-items: center; 59 gap: 5px; 60 } 61 .qty-btn { 62 padding: 6px 10px; 63 font-size: 1.2rem; 64 border: 1px solid #ccc; 65 background: #f9f9f9; 66 cursor: pointer; 67 border-radius: 4px; 68 } 69 .qty-btn:active { 70 background: #eee; 71 } 72 .qty-wrapper input { 73 width: 60px; 74 text-align: center; 75 } 76 </style> 77 78 <div> 79 <h1 class="my-3">@title</h1> 80 <div class="w-50">@description</div> 81 </div> 82 83 <div class="mt-4 grid"> 84 <div class="g-col-3"> 85 <h2>@Translate("Salesperson")</h2> 86 87 <div class="mt-3"> 88 <div id="contactPerson">Kontaktperson: <span>@salesPersonName</span></div> 89 <div id="contactMail">E-post: <span>@salesPersonMail</span></div> 90 </div> 91 </div> 92 93 @if (secondaryUser != null) 94 { 95 <div class="g-col-3"> 96 <h2>@Translate("Customer")</h2> 97 98 <div class="mt-3"> 99 <div>Attn: <span id="customCompany">@currentUser.Company</span></div> 100 <div>Kundnummer: <span>@currentUser.CustomerNumber</span></div> 101 <div>@currentUser.Name @currentUser.ID</div> 102 <div>@currentUser.Address, @currentUser.Zip @currentUser.City</div> 103 104 <div class="d-none" id="customerID">@currentUser.ID</div> 105 </div> 106 </div> 107 } 108 </div> 109 110 <div class="mt-4"> 111 <h2>@Translate("Settings")</h2> 112 113 <div class="grid mt-3"> 114 <div class="form-floating g-col-3"> 115 <input id="expires" class="form-control" type="date" name="expires" placeholder="" value=""> 116 <label for="expires" class="form-label">Giltig t.o.m</label> 117 </div> 118 119 <div class="form-floating g-col-3"> 120 <input id="offerName" class="form-control" type="text" name="offerName" placeholder="Namnge offert" value=""> 121 <label for="offerName" class="form-label">Namnge offert</label> 122 </div> 123 124 <div class="mb-3 g-col-12"> 125 <label for="terms" class="form-label fw-bold fs-5">Terms</label> 126 <br /> 127 <small>If not filled out a standard terms and conditions text will been shown in the offer.</small> 128 <textarea class="form-control" id="terms" rows="5"></textarea> 129 </div> 130 </div> 131 </div> 132 133 <div class="mt-4"> 134 <h2>@Translate("Products")</h2> 135 136 <div> 137 <table class="w-100"> 138 <thead> 139 <tr style="border-bottom: 2px solid #ccc; font-weight: bold;"> 140 <th style="padding: 10px 20px;"></th> 141 <th style="padding: 10px 20px; text-align:left;">Benämning</th> 142 <th style="padding: 10px 20px; text-align:left;">Art.nr</th> 143 <th style="padding: 10px 20px; text-align:center;">MOQ</th> 144 <th style="padding: 10px 20px; text-align:left;">Rek.pris/st</th> 145 <th style="padding: 10px 20px; text-align:left;">Pris netto/st</th> 146 <th style="padding: 10px 20px; text-align:left;">Rabatt %</th> 147 <th style="padding: 10px 20px; text-align:left;">Pris netto/st</th> 148 <th style="padding: 10px 20px; text-align:left;">Offererat pris</th> 149 </tr> 150 </thead> 151 <tbody> 152 @foreach (var item in products) 153 { 154 var product = Dynamicweb.Ecommerce.Services.Products.GetProductByNumber( 155 item.Number, 156 Dynamicweb.Ecommerce.Services.Languages.GetDefaultLanguageId() 157 ); 158 159 ProductImageService productImageService = new ProductImageService(); 160 var image = productImageService.GetImagePath(product); 161 162 var vmSettings = new ProductViewModelSettings 163 { 164 LanguageId = Dynamicweb.Ecommerce.Services.Languages.GetDefaultLanguageId(), 165 CurrencyCode = "SEK", 166 CountryCode = "SE", 167 ShopId = Dynamicweb.Ecommerce.Services.Shops.GetShop(Pageview.Area.EcomShopId).ToString(), 168 UserId = currentUser.ID 169 }; 170 171 var productVm = !string.IsNullOrWhiteSpace(item.VariantId) 172 ? ViewModelFactory.CreateView(vmSettings, product.Id, item.VariantId, null) 173 : ViewModelFactory.CreateView(vmSettings, product.Id); 174 175 var vmSettingsAnon = new ProductViewModelSettings 176 { 177 LanguageId = Dynamicweb.Ecommerce.Services.Languages.GetDefaultLanguageId(), 178 CurrencyCode = "SEK", 179 CountryCode = "SE", 180 ShopId = Dynamicweb.Ecommerce.Services.Shops.GetShop(Pageview.Area.EcomShopId).ToString(), 181 UserId = 0 182 }; 183 184 var anonVm = !string.IsNullOrWhiteSpace(item.VariantId) 185 ? ViewModelFactory.CreateView(vmSettingsAnon, product.Id, item.VariantId, null) 186 : ViewModelFactory.CreateView(vmSettingsAnon, product.Id); 187 188 var hasCustomerNo = !string.IsNullOrWhiteSpace(currentUser?.CustomerNumber); 189 var effectiveVm = hasCustomerNo ? productVm : anonVm; 190 191 var priceSteps = productVm.Prices? 192 .Select(p => $"{p.Quantity}:{p.Price.Price.ToString(CultureInfo.InvariantCulture)}"); 193 194 var priceBreaks = priceSteps != null ? string.Join(";", priceSteps) : ""; 195 196 <tr style="border-bottom: 1px solid grey; background-color: white;" class="product-item"> 197 <td style="padding: 10px 20px;"> 198 <img src="@image" width="70" /> 199 </td> 200 <td style="padding: 10px 20px;">@product.Name</td> 201 <td style="padding: 10px 20px;" class="product-number">@product.Number</td> 202 <td style="padding: 10px 20px;"> 203 <div class="qty-wrapper"> 204 <button type="button" class="qty-btn minus">−</button> 205 <input type="number" name="quantity" min="1" value="@item.Qty" /> 206 <button type="button" class="qty-btn plus">+</button> 207 </div> 208 </td> 209 <td style="padding: 10px 20px;" class="a-price" 210 data-a-raw-price="@anonVm.Price?.Price"> 211 <div>@anonVm.Price?.PriceFormattedNoSymbol</div> 212 </td> 213 <td style="padding: 10px 20px;" class="product-price" 214 data-raw-price="@effectiveVm.Price?.Price" 215 data-price-breaks="@priceBreaks"> 216 217 <div class="line-total">@effectiveVm.Price?.PriceFormattedNoSymbol</div> 218 <small class="unit-price text-muted"></small> 219 </td> 220 <td style="padding: 10px 20px;"> 221 <input type="text" name="discount" placeholder="Eks: 20" title="Skriv 20 for prosent" /> 222 </td> 223 <td style="padding: 10px 20px;"> 224 <input type="text" name="netprice" placeholder="Eks: 100" title="Nettopris per stk" /> 225 </td> 226 <td style="padding: 10px 20px;" class="offer-price"></td> 227 </tr> 228 } 229 </tbody> 230 </table> 231 </div> 232 233 <div class="text-end"> 234 <a href="#" id="resetOffer" class="btn btn-secondary mt-3 me-3">Reset offer</a> 235 <a href="#" id="generatePDF" class="btn btn-primary mt-3">Generate offer</a> 236 <div id="offer-status" style="margin-top: 10px; display: none;"></div> 237 </div> 238 </div> 239 240 <script> 241 document.addEventListener("DOMContentLoaded", () => { 242 const STORAGE_PREFIX = "offer-"; 243 const EXPIRES_KEY = STORAGE_PREFIX + "expires"; 244 const OFFER_NAME_KEY = STORAGE_PREFIX + "offerName"; 245 const TERMS_KEY = STORAGE_PREFIX + "terms"; 246 247 function clearOfferStorage() { 248 Object.keys(localStorage).forEach((key) => { 249 if (key.startsWith(STORAGE_PREFIX)) { 250 localStorage.removeItem(key); 251 } 252 }); 253 } 254 255 const expiresInput = document.getElementById("expires"); 256 if (expiresInput) { 257 const savedExpires = localStorage.getItem(EXPIRES_KEY); 258 259 if (savedExpires) { 260 expiresInput.value = savedExpires; 261 } else { 262 const d = new Date(); 263 d.setDate(d.getDate() + 30); 264 const yyyy = d.getFullYear(); 265 const mm = String(d.getMonth() + 1).padStart(2, '0'); 266 const dd = String(d.getDate()).padStart(2, '0'); 267 const defaultExpires = `${yyyy}-${mm}-${dd}`; 268 expiresInput.value = defaultExpires; 269 localStorage.setItem(EXPIRES_KEY, defaultExpires); 270 } 271 272 expiresInput.addEventListener("change", () => { 273 localStorage.setItem(EXPIRES_KEY, expiresInput.value || ""); 274 }); 275 } 276 277 const offerNameInput = document.getElementById("offerName"); 278 if (offerNameInput) { 279 const savedOfferName = localStorage.getItem(OFFER_NAME_KEY); 280 if (savedOfferName !== null) { 281 offerNameInput.value = savedOfferName; 282 } 283 284 offerNameInput.addEventListener("input", () => { 285 localStorage.setItem(OFFER_NAME_KEY, offerNameInput.value || ""); 286 }); 287 } 288 289 const termsInput = document.getElementById("terms"); 290 if (termsInput) { 291 const savedTerms = localStorage.getItem(TERMS_KEY); 292 if (savedTerms !== null) { 293 termsInput.value = savedTerms; 294 } 295 296 termsInput.addEventListener("input", () => { 297 localStorage.setItem(TERMS_KEY, termsInput.value || ""); 298 }); 299 } 300 301 function getUnitPriceForQuantity(priceEl, qty) { 302 const base = parseFloat((priceEl.dataset.rawPrice || "0").replace(",", ".")) || 0; 303 const breaksStr = priceEl.dataset.priceBreaks; 304 305 if (!breaksStr) return base; 306 307 let bestPrice = base; 308 let bestQty = 0; 309 310 breaksStr.split(";").forEach(pair => { 311 if (!pair) return; 312 const parts = pair.split(":"); 313 if (parts.length !== 2) return; 314 315 const q = parseInt(parts[0], 10); 316 const p = parseFloat((parts[1] || "").replace(",", ".")); 317 318 if (!isNaN(q) && !isNaN(p) && qty >= q && q >= bestQty) { 319 bestQty = q; 320 bestPrice = p; 321 } 322 }); 323 324 return bestPrice; 325 } 326 327 function updateOfferPrice(priceEl, discountInput, netPriceInput, offerPriceEl, quantityInput) { 328 if (!priceEl || !offerPriceEl || !quantityInput || !discountInput || !netPriceInput) return; 329 330 const qty = Math.max(1, parseInt(quantityInput.value || "1", 10)); 331 332 const unitPrice = getUnitPriceForQuantity(priceEl, qty); 333 const totalStandard = unitPrice * qty; 334 335 const fmt = (n) => new Intl.NumberFormat('sv-SE', { 336 style: 'currency', 337 currency: 'SEK' 338 }).format(n); 339 340 const lineTotalEl = priceEl.querySelector(".line-total"); 341 if (lineTotalEl) lineTotalEl.textContent = fmt(totalStandard); 342 343 const unitPriceEl = priceEl.querySelector(".unit-price"); 344 if (unitPriceEl) unitPriceEl.textContent = `(Unit: ${fmt(unitPrice)})`; 345 346 const discountRaw = (discountInput.value || "").replace(",", ".").trim(); 347 const discount = parseFloat(discountRaw); 348 349 const netRaw = (netPriceInput.value || "").replace(",", ".").trim(); 350 const net = parseFloat(netRaw); 351 352 const hasNet = netRaw !== "" && !isNaN(net) && net >= 0; 353 const hasDiscount = discountRaw !== "" && !isNaN(discount) && discount >= 0 && discount <= 100; 354 355 let totalOffer = totalStandard; 356 357 if (hasNet) { 358 totalOffer = net * qty; 359 360 if (discountInput.value !== "") discountInput.value = ""; 361 discountInput.disabled = true; 362 } else { 363 discountInput.disabled = false; 364 365 if (hasDiscount && discount > 0) { 366 totalOffer = totalStandard * (1 - discount / 100); 367 } 368 } 369 370 if (totalOffer < 0) totalOffer = 0; 371 offerPriceEl.innerHTML = fmt(totalOffer); 372 } 373 374 document.querySelectorAll(".qty-wrapper").forEach(wrapper => { 375 const input = wrapper.querySelector("input[name='quantity']"); 376 const minus = wrapper.querySelector(".minus"); 377 const plus = wrapper.querySelector(".plus"); 378 379 minus.addEventListener("click", () => { 380 let val = Math.max(1, parseInt(input.value || "1", 10) - 1); 381 input.value = val; 382 input.dispatchEvent(new Event("input")); 383 }); 384 385 plus.addEventListener("click", () => { 386 let val = Math.max(1, parseInt(input.value || "1", 10) + 1); 387 input.value = val; 388 input.dispatchEvent(new Event("input")); 389 }); 390 }); 391 392 document.querySelectorAll(".product-item").forEach(item => { 393 const priceEl = item.querySelector(".product-price"); 394 const offerPriceEl = item.querySelector(".offer-price"); 395 const quantityInput = item.querySelector("input[name='quantity']"); 396 const discountInput = item.querySelector("input[name='discount']"); 397 const netPriceInput = item.querySelector("input[name='netprice']"); 398 const numberEl = item.querySelector(".product-number"); 399 400 if (!priceEl || !offerPriceEl || !quantityInput || !discountInput || !netPriceInput || !numberEl) return; 401 402 const productNumber = (numberEl.textContent || "").trim(); 403 const qtyKey = `${STORAGE_PREFIX}qty-${productNumber}`; 404 const discountKey = `${STORAGE_PREFIX}discount-${productNumber}`; 405 const netKey = `${STORAGE_PREFIX}net-${productNumber}`; 406 407 const initialQtyFromServer = Math.max(1, parseInt(quantityInput.value || "1", 10)); 408 const savedQty = localStorage.getItem(qtyKey); 409 if (savedQty !== null && !isNaN(parseInt(savedQty, 10))) { 410 const saved = Math.max(1, parseInt(savedQty, 10)); 411 if (initialQtyFromServer <= 1) { 412 quantityInput.value = String(saved); 413 } 414 } 415 416 const savedDiscount = localStorage.getItem(discountKey); 417 if (savedDiscount !== null) discountInput.value = savedDiscount; 418 419 const savedNet = localStorage.getItem(netKey); 420 if (savedNet !== null) netPriceInput.value = savedNet; 421 422 if ((netPriceInput.value || "").trim() !== "") { 423 discountInput.value = ""; 424 discountInput.disabled = true; 425 } 426 427 if ((discountInput.value || "").trim() !== "") { 428 netPriceInput.value = ""; 429 netPriceInput.disabled = true; 430 } 431 432 updateOfferPrice(priceEl, discountInput, netPriceInput, offerPriceEl, quantityInput); 433 434 discountInput.addEventListener("input", () => { 435 const raw = (discountInput.value || "").trim(); 436 437 if (raw !== "") { 438 netPriceInput.value = ""; 439 netPriceInput.disabled = true; 440 localStorage.setItem(netKey, ""); 441 } else { 442 netPriceInput.disabled = false; 443 } 444 445 const val = parseFloat(discountInput.value.replace(",", ".")); 446 if (!isNaN(val) && val >= 0 && val <= 100) { 447 discountInput.style.color = ""; 448 discountInput.title = "Skriv 0–100 for rabatt i prosent"; 449 } else if (raw !== "") { 450 discountInput.style.color = "red"; 451 discountInput.title = "Ugyldig rabatt – må være mellom 0 og 100"; 452 } else { 453 discountInput.style.color = ""; 454 discountInput.title = "Skriv 0–100 for rabatt i prosent"; 455 } 456 457 localStorage.setItem(discountKey, discountInput.value || ""); 458 updateOfferPrice(priceEl, discountInput, netPriceInput, offerPriceEl, quantityInput); 459 }); 460 461 netPriceInput.addEventListener("input", () => { 462 const raw = (netPriceInput.value || "").trim(); 463 464 if (raw !== "") { 465 discountInput.value = ""; 466 discountInput.disabled = true; 467 localStorage.setItem(discountKey, ""); 468 } else { 469 discountInput.disabled = false; 470 } 471 472 const val = parseFloat(netPriceInput.value.replace(",", ".")); 473 if (!isNaN(val) && val >= 0) { 474 netPriceInput.style.color = ""; 475 netPriceInput.title = "Nettopris per stk (0 eller høyere)"; 476 } else if (raw !== "") { 477 netPriceInput.style.color = "red"; 478 netPriceInput.title = "Ugyldig nettopris – må være et tall (0 eller høyere)"; 479 } else { 480 netPriceInput.style.color = ""; 481 netPriceInput.title = "Nettopris per stk"; 482 } 483 484 localStorage.setItem(netKey, netPriceInput.value || ""); 485 updateOfferPrice(priceEl, discountInput, netPriceInput, offerPriceEl, quantityInput); 486 }); 487 488 quantityInput.addEventListener("input", () => { 489 const q = Math.max(1, parseInt(quantityInput.value || "1", 10)); 490 quantityInput.value = String(q); 491 localStorage.setItem(qtyKey, String(q)); 492 493 updateOfferPrice(priceEl, discountInput, netPriceInput, offerPriceEl, quantityInput); 494 }); 495 }); 496 497 function formatDateToDDMMYYYY(dateStr) { 498 if (!dateStr || !dateStr.includes("-")) return ""; 499 const parts = dateStr.split("-"); 500 if (parts.length !== 3) return ""; 501 const yyyy = parts[0]; 502 const mm = parts[1]; 503 const dd = parts[2]; 504 return `${dd}.${mm}.${yyyy}`; 505 } 506 507 function buildPdfUrl() { 508 const productLines = []; 509 510 document.querySelectorAll(".product-item").forEach(item => { 511 const numberEl = item.querySelector(".product-number"); 512 const quantityInput = item.querySelector("input[name='quantity']"); 513 const discountInput = item.querySelector("input[name='discount']"); 514 const netPriceInput = item.querySelector("input[name='netprice']"); 515 const priceEl = item.querySelector(".product-price"); 516 517 if (!numberEl || !quantityInput || !discountInput || !netPriceInput || !priceEl) return; 518 519 const productNumber = (numberEl.textContent || "").trim(); 520 const qty = Math.max(1, parseInt(quantityInput.value || "1", 10)); 521 522 const discountRaw = (discountInput.value || "").replace(",", ".").trim(); 523 const discount = parseFloat(discountRaw); 524 525 const netRaw = (netPriceInput.value || "").replace(",", ".").trim(); 526 const net = parseFloat(netRaw); 527 const hasNet = netRaw !== "" && !isNaN(net) && net >= 0; 528 529 let discountForUrl = "0"; 530 if (!hasNet && !isNaN(discount) && discount >= 0 && discount <= 100) { 531 discountForUrl = discount.toString().replace(".", ","); 532 } 533 534 const unitPrice = getUnitPriceForQuantity(priceEl, qty); 535 const totalStandard = unitPrice * qty; 536 537 let totalOffer = totalStandard; 538 539 if (hasNet) { 540 totalOffer = net * qty; 541 } else if (!isNaN(discount) && discount > 0 && discount <= 100) { 542 totalOffer = totalStandard * (1 - discount / 100); 543 } 544 545 if (totalOffer < 0) totalOffer = 0; 546 547 const netPriceForUrl = hasNet ? net.toFixed(2) : "0"; 548 const offerPriceForUrl = totalOffer.toFixed(2); 549 550 const line = `${productNumber}_${qty}_${discountForUrl}_${netPriceForUrl}_${offerPriceForUrl}`; 551 productLines.push(line); 552 }); 553 554 const contactPersonSpan = document.querySelector("#contactPerson span"); 555 const contactMailSpan = document.querySelector("#contactMail span"); 556 const customerIdEl = document.getElementById("customerID"); 557 558 const contactPerson = contactPersonSpan ? contactPersonSpan.textContent.trim() : ""; 559 const contactMail = contactMailSpan ? contactMailSpan.textContent.trim() : ""; 560 const customerId = customerIdEl ? customerIdEl.textContent.trim() : ""; 561 562 const expiresInput = document.getElementById("expires"); 563 const expiresRaw = expiresInput ? (expiresInput.value || "") : ""; 564 const expiresFormatted = formatDateToDDMMYYYY(expiresRaw); 565 566 const offerNameInput = document.getElementById("offerName"); 567 const offerNameRaw = offerNameInput ? (offerNameInput.value || "") : ""; 568 569 const termsInput = document.getElementById("terms"); 570 const termsRaw = termsInput ? (termsInput.value || "") : ""; 571 572 const baseUrl = "https://sternhammar.dw10staging.dynamicweb-cms.com/pdf"; 573 574 const url = 575 baseUrl + 576 "?products=" + encodeURIComponent(productLines.join(";")) + 577 "&contactPerson=" + encodeURIComponent(contactPerson) + 578 "&expires=" + encodeURIComponent(expiresFormatted) + 579 "&offerName=" + encodeURIComponent(offerNameRaw) + 580 "&terms=" + encodeURIComponent(termsRaw) + 581 "&customerId=" + encodeURIComponent(customerId); 582 583 return url; 584 } 585 586 function getCompanyNameForFilename() { 587 const el = document.getElementById("customCompany"); 588 const name = (el?.textContent || "").trim(); 589 return name && name.length > 0 ? name : "Offert"; 590 } 591 592 const generateBtn = document.getElementById("generatePDF"); 593 if (generateBtn) { 594 generateBtn.addEventListener("click", async (e) => { 595 e.preventDefault(); 596 597 const statusEl = document.getElementById("offer-status"); 598 599 generateBtn.disabled = true; 600 generateBtn.classList.add("disabled"); 601 if (statusEl) { 602 statusEl.style.display = "block"; 603 statusEl.textContent = "Sender tilbud på e-post …"; 604 } 605 606 try { 607 const pdfUrl = buildPdfUrl(); 608 if (!pdfUrl) throw new Error("Kunne ikke generere URL"); 609 610 const fileName = getCompanyNameForFilename(); 611 const contactMailText = 612 document.querySelector("#contactMail span")?.textContent.trim() || ""; 613 614 const webhookUrl = 615 "https://menntor.mennt.it/webhook/9d841d83-8f2b-4c52-a9ab-d28c5b48cdc0" + 616 "?url=" + encodeURIComponent(pdfUrl) + 617 "&filename=" + encodeURIComponent(fileName) + 618 "&contactMail=" + encodeURIComponent(contactMailText); 619 620 const response = await fetch(webhookUrl); 621 if (!response.ok) throw new Error("Feil ved sending"); 622 623 if (statusEl) { 624 statusEl.textContent = "Offerten er sendt på e-post."; 625 } 626 627 clearOfferStorage(); 628 } catch (err) { 629 console.error(err); 630 631 if (statusEl) { 632 statusEl.textContent = "Noe gikk galt under sending av tilbudet. Prøv igjen."; 633 } 634 635 generateBtn.disabled = false; 636 generateBtn.classList.remove("disabled"); 637 } 638 }); 639 } 640 641 const resetBtn = document.getElementById("resetOffer"); 642 if (resetBtn) { 643 resetBtn.addEventListener("click", (e) => { 644 e.preventDefault(); 645 646 clearOfferStorage(); 647 648 if (expiresInput) { 649 const d = new Date(); 650 d.setDate(d.getDate() + 30); 651 const yyyy = d.getFullYear(); 652 const mm = String(d.getMonth() + 1).padStart(2, '0'); 653 const dd = String(d.getDate()).padStart(2, '0'); 654 const defaultExpires = `${yyyy}-${mm}-${dd}`; 655 expiresInput.value = defaultExpires; 656 localStorage.setItem(EXPIRES_KEY, defaultExpires); 657 } 658 659 if (offerNameInput) { 660 offerNameInput.value = ""; 661 localStorage.setItem(OFFER_NAME_KEY, ""); 662 } 663 664 if (termsInput) { 665 termsInput.value = ""; 666 } 667 668 document.querySelectorAll(".product-item").forEach(item => { 669 const priceEl = item.querySelector(".product-price"); 670 const offerPriceEl = item.querySelector(".offer-price"); 671 const quantityInput = item.querySelector("input[name='quantity']"); 672 const discountInput = item.querySelector("input[name='discount']"); 673 const netPriceInput = item.querySelector("input[name='netprice']"); 674 675 if (!priceEl || !offerPriceEl || !quantityInput || !discountInput || !netPriceInput) return; 676 677 quantityInput.value = "1"; 678 discountInput.value = ""; 679 netPriceInput.value = ""; 680 681 discountInput.disabled = false; 682 netPriceInput.disabled = false; 683 684 updateOfferPrice(priceEl, discountInput, netPriceInput, offerPriceEl, quantityInput); 685 }); 686 }); 687 } 688 }); 689 </script>
By clicking 'Accept All' you consent that we may collect information about you for various purposes, including: Statistics and Marketing